diff --git a/packages/backend/src/plugins/workflow-argo.ts b/packages/backend/src/plugins/workflow-argo.ts index eaec485..f99d497 100644 --- a/packages/backend/src/plugins/workflow-argo.ts +++ b/packages/backend/src/plugins/workflow-argo.ts @@ -4,29 +4,77 @@ import * as k8s from '@kubernetes/client-node'; import {Logger} from "winston"; import {HttpError} from "@kubernetes/client-node"; -// export function createInvokeArgoAction() { - - type argoInput = { - name: string namespace: string clusterName: string + templateName: string + parameters: parameter[] } + +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 + + 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 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: - 'Append content to the end of the given file, it will create the file if it does not exist.', + 'Invokes an Argo workflow using a workflow template', schema: { input: { type: 'object', - required: ['name', 'namespace', 'clusterName'], + required: ['namespace', 'clusterName', 'templateName'], properties: { - name: { - title: 'Name', - description: 'Name of Argo workflow template', - type: 'string', - }, namespace: { title: 'Namespace', description: 'Namespace to run this workflow', @@ -37,13 +85,34 @@ export function createInvokeArgoAction(config: Config, logger: Logger) { description: 'Name of Cluster', type: 'string', }, + templateName: { + title: 'Template name', + description: 'Argo Workflows template name', + 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" + } + } + } + } }, }, output: { type: 'object', properties: { ID: { - title: 'ID', + title: 'Workflow ID', type: 'string', }, }, @@ -52,26 +121,7 @@ export function createInvokeArgoAction(config: Config, logger: Logger) { async handler(ctx: ActionContext) { logger.debug(`Invoked with ${ctx.input}`) - const c = config.getConfigArray("kubernetes.clusterLocatorMethods") - const cc = c.filter(function(val) { - return val.getString("type") === "config" - }) - logger.info(`found ${cc.length} statically configured clusters`) - - const clusters = new Array(); - // this is shit - cc.forEach(function(conf ) { - const cl = conf.getConfigArray("clusters") - cl.forEach(function(val) { - if (val.getString("name") === ctx.input.clusterName) { - clusters.push(val) - } - }) - }) - if (clusters.length === 0 ) { - throw new Error("Cluster not found") - } - const targetCluster = clusters[0] + const targetCluster = getClusterConfig(ctx.input.clusterName, config) const kc = new k8s.KubeConfig() kc.addCluster({ name: ctx.input.clusterName, @@ -89,23 +139,85 @@ export function createInvokeArgoAction(config: Config, logger: Logger) { name: ctx.input.clusterName }) kc.setCurrentContext(ctx.input.clusterName) - const client = kc.makeApiClient(k8s.CoreV1Api) - logger.info("made client") - try { - const resp = await client.listNamespace() - logger.info(`response: ${resp.body}`) - } catch (error) { - if (error instanceof HttpError) { - logger.info(`error : ${error.response.statusCode} ${error.response.statusMessage}`) - throw new Error(`Failed to talk to the cluster: ${error.response.statusCode} ${error.response.statusMessage}`) + 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, body + ) + logger.debug(`response: ${resp.body}`) + ctx.output('ID', resp.body.toString()) + } 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 (error instanceof Error) { - logger.error(`error while talking to cluster: ${error.name} ${error.message}`) + if (err instanceof Error) { + logger.error(`error while talking to cluster: ${err.name} ${err.message}`) } throw new Error("Unknown exception was encountered.") } } } ) -} \ No newline at end of file +} + +function getClusterConfig(name: string, config: Config): Config { + const c = config.getConfigArray("kubernetes.clusterLocatorMethods") + const cc = c.filter(function(val) { + return val.getString("type") === "config" + }) + + const clusters = new Array(); + // this is shit + cc.forEach(function(conf ) { + const cl = conf.getConfigArray("clusters") + cl.forEach(function(val) { + if (val.getString("name") === name) { + clusters.push(val) + } + }) + }) + if (clusters.length === 0 ) { + throw new Error(`Cluster with name ${name} not found`) + } + return clusters[0] +} + +function generateBody(templateName: string, namespace: string, entrypoint: string): object { + let obj = { + "apiVersion": "argoproj.io/v1alpha1", + "kind": "Workflow", + "metadata": { + "generateName": "backstage-scaffolding-", + "namespace": `${namespace}` + }, + "spec": { + "arguments": { + "parameters": [ + { + "name": "message", + "value": "from workflow" + } + ] + }, + "workflowTemplateRef": { + "name": `${templateName}` + } + } + } + if (entrypoint) { + obj.spec.entorypoint = entrypoint + } + return obj +} + + diff --git a/test-template.yaml b/test-template.yaml new file mode 100644 index 0000000..2972d0c --- /dev/null +++ b/test-template.yaml @@ -0,0 +1,80 @@ +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: test-template + title: TESTING + description: test +spec: + owner: backstage/techdocs-core + type: service + # these are the steps which are rendered in the frontend with the form input + parameters: + - title: Fill in some steps + required: + - name + - owner + properties: + name: + title: Application Name + type: string + description: Unique name of the component + ui:autofocus: true + ui:options: + rows: 5 + owner: + title: Owner + type: string + description: Owner of the component + ui:field: OwnerPicker + ui:options: + catalogFilter: + kind: Group + labels: + title: Labels + type: object + additionalProperties: + type: string + description: Labels to apply to the application + namespace: + title: Namespace + type: string + description: Namespace to deploy this application into. Optional. Defaults to application name. + ui:options: + rows: 5 + clusterName: + title: Cluster Name + type: string + default: canoe-packaging + description: Name of the cluster to run this in + - title: Workflow params + properties: + workflowParams: + title: workflow parameters + type: array + description: workflow parameters + ui:autofocus: true + items: + type: object + properties: + required: + - name + - value + name: + type: string + value: + type: string + steps: + - id: flow + name: Flow + action: workflows:argo:invoke + input: + templateName: workflow-template-whalesay-template + namespace: admin + clusterName: ${{ parameters.clusterName }} + parameters: ${{ parameters.workflowParams }} + +# output: +# links: +# - title: Open in catalog +# icon: catalog +# entityRef: ${{ steps['register'].output.entityRef }}