From 88584afd31298b12e6376bf64591a9a52f646ef9 Mon Sep 17 00:00:00 2001 From: Manabu Mccloskey Date: Fri, 29 Sep 2023 17:27:55 -0700 Subject: [PATCH] enable argocd integration --- packages/app/package.json | 1 + .../app/src/components/catalog/EntityPage.tsx | 9 + packages/backend/src/plugins/scaffolder.ts | 2 - packages/backend/src/plugins/workflow-argo.ts | 264 ------------------ 4 files changed, 10 insertions(+), 266 deletions(-) delete mode 100644 packages/backend/src/plugins/workflow-argo.ts diff --git a/packages/app/package.json b/packages/app/package.json index b64336c..908bd8b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -54,6 +54,7 @@ "@material-ui/icons": "^4.9.1", "@rjsf/core": "^5.8.1", "@rjsf/utils": "^5.8.1", + "@roadiehq/backstage-plugin-argo-cd": "^2.3.4", "@types/fs-extra": "^11.0.1", "fs-extra": "^11.1.1", "history": "^5.0.0", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 318a4b3..e8a1414 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -64,6 +64,10 @@ import { isArgoWorkflowsAvailable, } from '@cnoe-io/plugin-argo-workflows'; import { ApacheSparkPage } from '@cnoe-io/plugin-apache-spark'; +import { + EntityArgoCDHistoryCard, + isArgocdAvailable, +} from '@roadiehq/backstage-plugin-argo-cd'; const techdocsContent = ( @@ -134,6 +138,11 @@ const overviewContent = ( + Boolean(isArgocdAvailable(e))}> + + + + diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 49eeb2d..536375a 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -6,7 +6,6 @@ import { import { Router } from 'express'; import type { PluginEnvironment } from '../types'; import { ScmIntegrations } from '@backstage/integration'; -import { createInvokeArgoAction } from './workflow-argo'; import { createZipAction, createSleepAction, @@ -69,7 +68,6 @@ export default async function createPlugin( ...builtInActions, ...scaffolderBackendModuleUtils, ...cnoeActions, - createInvokeArgoAction(env.config, env.logger), ]; return await createRouter({ diff --git a/packages/backend/src/plugins/workflow-argo.ts b/packages/backend/src/plugins/workflow-argo.ts deleted file mode 100644 index e4125d9..0000000 --- a/packages/backend/src/plugins/workflow-argo.ts +++ /dev/null @@ -1,264 +0,0 @@ -import {ActionContext, createTemplateAction} from "@backstage/plugin-scaffolder-node"; -import { Config } from '@backstage/config'; -import * as k8s from '@kubernetes/client-node'; -import {Logger} from "winston"; -import {HttpError} from "@kubernetes/client-node"; - -type argoInput = { - namespace: string - clusterName: string - userOIDCToken: string - templateName: string - parameters: parameter[] - wait?: boolean -} - -const argoWorkflowsGroup = 'argoproj.io' -const argoWorkflowsVersion = 'v1alpha1' -const argoWorkFlowPlural = 'workflows' -const argoWorkFlowKind = 'Workflow' -const argoWorkFlowMetadataDefault: k8s.V1ObjectMeta = { - generateName: "backstage-scaffolding-" -} - -class Workflow { - apiVersion: string = `${argoWorkflowsGroup}/${argoWorkflowsVersion}` - kind: string = argoWorkFlowKind - metadata: k8s.V1ObjectMeta = argoWorkFlowMetadataDefault - spec: workflowSpec - status?: workflowStatus - constructor(templateName: string, namespace: string, params?: parameter[], artifacts?: object[] ) { - this.metadata.namespace = namespace - const args: argument = {} - if (params) { - args.parameters = params - } - if (artifacts) { - args.artifacts = artifacts - } - this.spec = { - workflowTemplateRef: { - name: templateName - }, - arguments: args - } - } -} - -type workflowSpec = { - arguments?: argument - entrypoint?: string - workflowTemplateRef: workflowTemplateRef -} - -type workflowStatus = { - conditions?: workflowStatusCondition[] - phase?: string - progress?: string -} - -type workflowStatusCondition = { - message?: string - status?: string - type: string -} - -type workflowTemplateRef = { - clusterScope?: boolean - name: string -} - -type argument = { - artifacts?: object[] - parameters?: parameter[] -} - -type parameter = { - name: string - value: string - valueFrom?: object -} - -export function createInvokeArgoAction(config: Config, logger: Logger) { - return createTemplateAction({ - id: 'workflows:argo:invoke', - description: - 'Invokes an Argo workflow using a workflow template', - schema: { - input: { - type: 'object', - required: ['namespace', 'clusterName', 'templateName'], - properties: { - namespace: { - title: 'Namespace', - description: 'Namespace to run this workflow', - type: 'string', - }, - clusterName: { - title: 'Cluster name', - description: 'Name of Cluster', - type: 'string', - }, - userOIDCToken: { - title: 'User\'s OIDC token', - description: "If specified, it will use the provided token to communicate with the Kubernetes cluster", - type: 'string' - }, - templateName: { - title: 'Template name', - description: 'Argo Workflows template name to run', - type: 'string', - }, - parameters: { - title: "Argo workflows parameters", - description: 'parameters used by the template', - type: 'array', - items: { - type: "object", - properties: { - name: { - type: "string" - }, - value: { - type: "string" - } - } - } - }, - wait: { - title: 'Wait for completion', - description: 'specify weather to wait for completion of this workflow.', - type: 'boolean', - } - }, - }, - output: { - type: 'object', - properties: { - workflowName: { - title: 'Workflow name', - type: 'string', - }, - workflowNamespace: { - title: 'Workflow namespace', - type: 'string', - }, - }, - }, - }, - async handler(ctx: ActionContext) { - logger.debug(`Invoked with ${JSON.stringify(ctx.input)})`) - logger.info(JSON.stringify(ctx.secrets)) - const targetCluster = getClusterConfig(ctx.input.clusterName, config) - const kc = new k8s.KubeConfig() - kc.addCluster({ - name: targetCluster.getString("name"), - caData: targetCluster.getString("caData"), - server: targetCluster.getString("url"), - skipTLSVerify: targetCluster.getBoolean("skipTLSVerify"), - }) - - kc.addUser({ - name: "scaffolder-user", - token: ctx.input.userOIDCToken? ctx.input.userOIDCToken : targetCluster.getString("serviceAccountToken") - }) - kc.addContext({ - cluster: ctx.input.clusterName, - user: "scaffolder-user", - name: ctx.input.clusterName - }) - kc.setCurrentContext(ctx.input.clusterName) - - const client = kc.makeApiClient(k8s.CustomObjectsApi) - const wf = new Workflow(ctx.input.templateName, ctx.input.namespace, ctx.input.parameters) - // const body = generateBody(ctx.input.templateName, ctx.input.namespace) - try { - const resp = await client.createNamespacedCustomObject( - argoWorkflowsGroup, argoWorkflowsVersion, ctx.input.namespace, - argoWorkFlowPlural, wf - ) - const respBody = resp.body as Workflow - logger.debug(`Workflow ID: ${respBody.metadata.name}, namespace ${respBody.metadata.namespace}`) - ctx.output('workflowName', respBody.metadata.name!) - ctx.output('workflowNamespace', respBody.metadata.namespace!) - if (ctx.input.wait) { - await wait(kc, respBody.metadata.namespace!, respBody.metadata.name!) - } - } catch (err) { - if (err instanceof HttpError) { - let msg = `${err.response.statusMessage}: ` - if ("kind" in err.body && err.body.kind === "Status" && "message" in err.body) { - msg += err.body.message - } - logger.info(`error : ${err.response.statusCode} ${msg}`) - throw new Error(`Failed to talk to the cluster: ${err.response.statusCode} ${err.response.statusMessage} \n ${msg}`) - } - if (err instanceof Error) { - logger.error(`error while talking to cluster: ${err.name} ${err.message}`) - } - throw new Error("Unknown exception was encountered.") - } - } - } - ) -} - -function getClusterConfig(name: string, config: Config): Config { - - const clusterConfigs = config.getConfigArray("kubernetes.clusterLocatorMethods").filter( - (val: Config) => { - return val.getString('type') === 'config' - } - ) - - const clusters = new Array(); - clusterConfigs.filter( (conf: Config) => { - const cluster = conf.getConfigArray("clusters").find( (val: Config) => { - return val.getString("name") === name - }) - if (cluster) { - clusters.push(cluster) - } - }) - - if (clusters.length === 0 ) { - throw new Error(`Cluster with name ${name} not found`) - } - return clusters[0] -} - -async function wait(kc: k8s.KubeConfig, namespace: string, name: string, timeoutSeconds: number = 120) { - const client = new k8s.Watch(kc) - return new Promise( async (resolve, reject) => { - const result = await client.watch( - `/apis/${argoWorkflowsGroup}/${argoWorkflowsVersion}/namespaces/${namespace}/${argoWorkFlowPlural}`, - { - fieldSelector: `metadata.name=${name}`, - }, - (_type, apiObj, _watchObj) => { - if (apiObj) { - const wf = apiObj as Workflow - if (wf.status && wf.status.conditions) { - const cond = wf.status.conditions.filter((val) => { - return val.type === 'Completed' && val.status === "True" - }) - if (cond.length > 0) { - // result.abort() - resolve() - return - } - } - } - }, - (err) => { - if (err instanceof Error) { - // logger.debug(`error encountered while waiting for workflow to complete: ${err.name} ${err.message}`) - } - } - ) - setTimeout(() => { - result.abort() - reject(new Error("TIMEOUT")) - }, timeoutSeconds * 1000) - }) -}