Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP RHTAP-2538 #211

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 42 additions & 13 deletions src/apis/git-providers/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ export class GitLabProvider extends Utils {
private readonly gitlab;
private readonly gitlabToken;
private readonly extractImagePatternFromGitopsManifest;
private readonly jenkinsAgentImage="image-registry.openshift-image-registry.svc:5000/jenkins/jenkins-agent-base:latest";
//Uncomment this, in case you want to build image for Jenkins Agent
//private readonly jenkinsAgentImage="image-registry.openshift-image-registry.svc:5000/jenkins/jenkins-agent-base:latest";
private readonly jenkinsAgentImage="quay.io/jkopriva/rhtap-jenkins-agent:0.1";

constructor(gitlabToken: string) {
super()
Expand All @@ -27,20 +29,20 @@ export class GitLabProvider extends Utils {
}

// Function to find a repository by name
public async checkIfRepositoryExists(namespace: string, repoName: string): Promise<number> {
public async checkIfRepositoryExists(organization: string, repoName: string): Promise<number> {
//RHTAPBUGS-1327: Added wait: it should improve stability of Gitlab test - sometimes request from tests could be faster, than GitLab responses
while (true) {
try {
const projects = await this.gitlab.Projects.show(`${namespace}/${repoName}`);
const projects = await this.gitlab.Projects.show(`${organization}/${repoName}`);
if (projects) {
console.info(`Repository with name '${repoName}' found in organization '${namespace}'
created at '${projects.created_at}' url: gitlab.com/${namespace}/${repoName}`);
console.info(`Repository with name '${repoName}' found in organization '${organization}'
created at '${projects.created_at}' url: gitlab.com/${organization}/${repoName}`);
return projects.id
}

await this.sleep(10000); // Wait 10 seconds before checking again
} catch (error) {
console.info(`Failed to check if repository ${repoName} exists`);
console.info(`Failed to check if repository ${organization}/${repoName} exists`);
}
}
}
Expand Down Expand Up @@ -109,15 +111,14 @@ export class GitLabProvider extends Utils {
}
}


public async updateJenkinsfileAgent(repositoryID: number, branchName: string): Promise<boolean> {
let stringToFind = "agent any";
let replacementString = "agent {\n kubernetes {\n label 'jenkins-agent'\n cloud 'openshift'\n serviceAccount 'jenkins'\n podRetention onFailure()\n idleMinutes '5'\n containerTemplate {\n name 'jnlp'\n image '" + this.jenkinsAgentImage + ":latest'\n ttyEnabled true\n args '${computer.jnlpmac} ${computer.name}'\n }\n } \n}";
let replacementString = "agent {\n kubernetes {\n label 'jenkins-agent'\n cloud 'openshift'\n serviceAccount 'jenkins'\n podRetention onFailure()\n idleMinutes '5'\n containerTemplate {\n name 'jnlp'\n image '" + this.jenkinsAgentImage + "'\n ttyEnabled true\n args '${computer.jnlpmac} ${computer.name}'\n }\n } \n}";
return await this.commitReplacementStringInFile(repositoryID, branchName, 'Jenkinsfile', 'Update Jenkins agent', stringToFind, replacementString);
}

public async createUsernameCommit(repositoryID: number, branchName: string): Promise<boolean> {
let stringToFind = "/* GITOPS_AUTH_USERNAME = credentials('GITOPS_AUTH_USERNAME') Uncomment this when using GitLab */"
let stringToFind = "/* GITOPS_AUTH_USERNAME = credentials('GITOPS_AUTH_USERNAME') */"
let replacementString = `GITOPS_AUTH_USERNAME = credentials('GITOPS_AUTH_USERNAME')`
return await this.commitReplacementStringInFile(repositoryID, branchName, 'Jenkinsfile', 'Update creds for Gitlab', stringToFind, replacementString);
}
Expand Down Expand Up @@ -193,7 +194,7 @@ export class GitLabProvider extends Utils {
repositoryID,
webHookUrl,
{
token: process.env.GITLAB_WEBHOOK_SECRET || '',
token: process.env.GITLAB_WEBHOOK_SECRET ?? '',
pushEvents: true,
mergeRequestsEvents: true,
tagPushEvents: true,
Expand Down Expand Up @@ -247,6 +248,7 @@ export class GitLabProvider extends Utils {
*/
public async mergeMergeRequest(projectId: number, mergeRequestId: number) {
try {
console.log(`Merging merge request "${mergeRequestId}"`);
await this.gitlab.MergeRequests.accept(projectId, mergeRequestId);

console.log(`Pull request "${mergeRequestId}" merged successfully.`);
Expand All @@ -256,6 +258,33 @@ export class GitLabProvider extends Utils {
}
}

/**
* Wait until merge request have mergeable status
*
* @param {number} projectId - The ID number of GitLab repo.
* @param {number} mergeRequestId - The ID number of GitLab merge request.
*/
public async waitForMergeableMergeRequest(projectId: number, mergeRequestId: number, timeoutMs: number) {
console.log(`Waiting for new pipeline to be created...`);
const retryInterval = 10 * 1000;
let totalTimeMs = 0;

while (timeoutMs === 0 || totalTimeMs < timeoutMs) {
try {
const detailedStatus = (await this.gitlab.MergeRequests.show(projectId, mergeRequestId)).detailed_merge_status
if(detailedStatus.toString() == "mergeable"){
return
}

await this.sleep(5000); // Wait 5 seconds
} catch (error) {
console.error('Error checking merge status:', error);
await new Promise(resolve => setTimeout(resolve, 15000)); // Wait for 15 seconds
}
totalTimeMs += retryInterval;
}
}

/**
* Delete project with ID from GitLab org.
*
Expand Down Expand Up @@ -370,11 +399,11 @@ export class GitLabProvider extends Utils {
}

public async updateRekorHost(repositoryID: number, branchName: string, rekorHost: string): Promise<boolean> {
return await this.commitReplacementStringInFile(repositoryID, branchName, 'rhtap/env.sh', 'Update Rekor host', `rekor-server.rhtap-tas.svc`, rekorHost);
return await this.commitReplacementStringInFile(repositoryID, branchName, 'rhtap/env.sh', 'Update Rekor host', `http://rekor-server.rhtap-tas.svc`, rekorHost);
}

public async updateTufMirror(repositoryID: number, branchName: string, tufMirror: string): Promise<boolean> {
return await this.commitReplacementStringInFile(repositoryID, branchName, 'rhtap/env.sh', 'Update TUF Mirror', `tuf.rhtap-tas.svc`, tufMirror);
return await this.commitReplacementStringInFile(repositoryID, branchName, 'rhtap/env.sh', 'Update TUF Mirror', `http://tuf.rhtap-tas.svc`, tufMirror);
}

public async updateEnvFileForGitLabCI(repositoryID: number, branchName: string, rekorHost: string, tufMirror: string): Promise<boolean> {
Expand Down Expand Up @@ -415,7 +444,7 @@ export class GitLabProvider extends Utils {
}
);

console.log(`${filePath} updated successfully for username.`);
console.log(`${filePath} updated successfully with commit message: ${commitMessage}`);
return true;
} catch (error: any) {
console.error('Error updating ${filePath}:', error);
Expand Down
2 changes: 1 addition & 1 deletion src/apis/git-providers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as path from "node:path";
export class Utils {
private readonly artifactDir: string
constructor() {
this.artifactDir = process.env.ARTIFACT_DIR || path.join(__dirname, '../../../', 'artifacts')
this.artifactDir = process.env.ARTIFACT_DIR ?? path.join(__dirname, '../../../', 'artifacts')
}

public async writeLogsToArtifactDir(storeDirectory: string, fileName: string, logData: string) {
Expand Down
71 changes: 60 additions & 11 deletions src/apis/kubernetes/kube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { OpenshiftRoute } from "./types/oc.routes.cr";
/**
* Constants for interacting with Kubernetes/OpenShift clusters.
*/
const RHTAPRootNamespace = process.env.RHTAP_ROOT_NAMESPACE || 'rhtap';
const RHTAPRootNamespace = process.env.RHTAP_ROOT_NAMESPACE ?? 'rhtap';

/**
* Kubernetes class for interacting with Kubernetes/OpenShift clusters.
Expand Down Expand Up @@ -39,7 +39,7 @@ export class Kubernetes extends Utils {
try {
const response = await k8sCoreApi.readNamespace(name)

if (response.body && response.body.metadata && response.body.metadata.name === name) {
if (response?.body?.metadata?.name === name) {
return true
}

Expand All @@ -61,8 +61,7 @@ export class Kubernetes extends Utils {
try {
const { body: taskRunList } = await customObjectsApi.listClusterCustomObject('tekton.dev', 'v1', 'taskruns');
const taskRunInterface = taskRunList as TaskRunList;
return taskRunInterface.items.filter(taskRun =>
taskRun.metadata && taskRun.metadata.name && taskRun.metadata.name.startsWith(pipelineRunName));
return taskRunInterface.items.filter(taskRun => taskRun?.metadata?.name?.startsWith(pipelineRunName));

} catch (error) {
console.error(error)
Expand Down Expand Up @@ -104,7 +103,7 @@ export class Kubernetes extends Utils {
const { body: pod } = await k8sApi.readNamespacedPod(podName, namespace);

// Check if pod.spec is defined
if (pod.spec && pod.spec.containers) {
if (pod.spec?.containers) {
// Iterate over each container in the pod
for (const container of pod.spec.containers) {
// Get logs from each container
Expand All @@ -124,15 +123,15 @@ export class Kubernetes extends Utils {
}
}

/**
/**
* Reads logs from a particular container from a specified pod and namespace and return logs
*
* @param {string} podName - The name of the pod.
* @param {string} namespace - The namespace where the pod is located.
* @param {string} ContainerName - The name of the Container.
* @returns {Promise<any>} A Promise that resolves once the logs are read and written to artifact files and return logs
*/
async readContainerLogs(podName: string, namespace: string, containerName: string): Promise<any> {
async readContainerLogs(podName: string, namespace: string, containerName: string): Promise<any> {
const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api);
try {
// Get logs from the given container
Expand Down Expand Up @@ -212,7 +211,7 @@ export class Kubernetes extends Utils {
const { body } = await customObjectsApi.getNamespacedCustomObject('tekton.dev', 'v1', namespace, 'pipelineruns', name);
const pr = body as PipelineRunKind;

if (pr.status && pr.status.conditions) {
if (pr.status?.conditions) {
const pipelineHasFinishedSuccessfully = pr.status.conditions.some(
(condition) => condition.status === 'True' && condition.type === 'Succeeded'
);
Expand Down Expand Up @@ -246,7 +245,7 @@ export class Kubernetes extends Utils {
* @param {string} name - The name of the pipelinerun
* @throws This function does not throw directly, but may throw errors during API calls or retries.
*/
public async pipelinerunfromName(name: string,namespace: string) {
public async pipelinerunfromName(name: string, namespace: string) {
try {
const k8sCoreApi = this.kubeConfig.makeApiClient(CustomObjectsApi);
const plr = await k8sCoreApi.getNamespacedCustomObject(
Expand Down Expand Up @@ -282,8 +281,8 @@ export class Kubernetes extends Utils {
const { body } = await customObjectsApi.getNamespacedCustomObject('argoproj.io', 'v1alpha1', RHTAPRootNamespace, 'applications', name);
const application = body as ApplicationSpec;

if (application.status && application.status.sync && application.status.sync.status &&
application.status.health && application.status.health.status) {
if (application.status?.sync?.status &&
application.status.health?.status) {

if (application.status.sync.status === 'Synced' && application.status.health.status === 'Healthy') {
return true;
Expand Down Expand Up @@ -480,4 +479,54 @@ export class Kubernetes extends Utils {
public async getTUFUrl(namespace: string): Promise<string> {
return this.getDeveloperHubSecret(namespace, "rhtap-tas-integration", "tuf_url");
}

/**
* Gets bombastic api URL.
*
* @param {string} namespace - The namespace where the route is located.
* @returns {Promise<string>} - returns route URL.
*/
public async getTTrustificationBombasticApiUrl(namespace: string): Promise<string> {
return this.getDeveloperHubSecret(namespace, "rhtap-trustification-integration", "bombastic_api_url");
}

/**
* Gets oidc issuer URL.
*
* @param {string} namespace - The namespace where the route is located.
* @returns {Promise<string>} - returns route URL.
*/
public async getTTrustificationOidcIssuerUrl(namespace: string): Promise<string> {
return this.getDeveloperHubSecret(namespace, "rhtap-trustification-integration", "oidc_issuer_url");
}

/**
* Gets oidc client ID.
*
* @param {string} namespace - The namespace where the route is located.
* @returns {Promise<string>} - returns route URL.
*/
public async getTTrustificationClientId(namespace: string): Promise<string> {
return this.getDeveloperHubSecret(namespace, "rhtap-trustification-integration", "oidc_client_id");
}

/**
* Gets oidc client secret.
*
* @param {string} namespace - The namespace where the route is located.
* @returns {Promise<string>} - returns route URL.
*/
public async getTTrustificationClientSecret(namespace: string): Promise<string> {
return this.getDeveloperHubSecret(namespace, "rhtap-trustification-integration", "oidc_client_secret");
}

/**
* Gets supported cyclone dx version.
*
* @param {string} namespace - The namespace where the route is located.
* @returns {Promise<string>} - returns route URL.
*/
public async getTTrustificationSupportedCycloneDXVersion(namespace: string): Promise<string> {
return this.getDeveloperHubSecret(namespace, "rhtap-trustification-integration", "supported_cyclonedx_version");
}
}
99 changes: 99 additions & 0 deletions src/apis/trustification/trustification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import axios from 'axios';
import { Utils } from '../git-providers/utils';


export class TrustificationClient extends Utils {

// Trustification client details
private readonly bombasticApiUrl: string;
private readonly oidcIssuesUrl: string;
private readonly oidcclientId: string;
private readonly oidcclientSecret: string;
private tpaToken: string;

/**
* Constructs a new instance of Trustification client.
*
*/
constructor(bombasticApiUrl: string, oidcIssuesUrl: string, oidcclientId: string, oidcclientSecret: string) {
super();
this.bombasticApiUrl = bombasticApiUrl;
this.oidcIssuesUrl = oidcIssuesUrl;
this.oidcclientId = oidcclientId;
this.oidcclientSecret = oidcclientSecret;
this.tpaToken = "";
}

public async initializeTpaToken() {

try {

const response = await axios.post(
this.oidcIssuesUrl + "/protocol/openid-connect/token",
{
client_id: this.oidcclientId,
client_secret: this.oidcclientSecret,
grant_type: 'client_credentials'
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
if (response?.data?.access_token) {
this.tpaToken = response.data.access_token;
console.log(`TPA token is set for trustification`);
}

} catch (error) {
console.error('Error getting TPA token', error);
throw error;
}

}


// Function to search for SBOM by name and wait until results are not empty
public async waitForSbomSearchByName(name: string, timeout: number = 300000, pollingInterval: number = 5000): Promise<any[]> {
const searchUrl = this.bombasticApiUrl + "/api/v1/sbom/search";
const startTime = Date.now();

while (true) {
// Timeout check
if (Date.now() - startTime > timeout) {
throw new Error(`Timeout: No SBOM found for '${name}' within ${timeout / 1000} seconds.`);
}

try {
// Perform GET request to search for SBOM by name
const response = await axios.get(searchUrl, {
headers: {
Authorization: `Bearer ${this.tpaToken}`,
Accept: '*/*'
},
params: {
q: name,
},
});

if (response.status === 200 && response.data.result && response.data.result.length > 0) {
console.log(`SBOM for '${name}' retrieved successfully. Found ${response.data.result.length} result(s).`);
return response.data.result;
}

console.log(`No SBOM found for '${name}' yet. Retrying...`);
} catch (error) {
console.error('Error searching for SBOM:', error);

// If it's a non-retryable error, throw it
if (!axios.isAxiosError(error) || (error.response && error.response.status !== 404)) {
throw error;
}
}

// Wait for the next polling interval
await this.sleep(pollingInterval);
}
}
}
Loading