This commit is contained in:
Manabu Mccloskey 2023-02-24 11:18:27 -08:00
parent 5ebdf36dde
commit 0e2fa2b27f
2 changed files with 236 additions and 44 deletions

View file

@ -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<argoInput>({
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<argoInput>) {
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<Config>();
// 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.")
}
}
}
)
}
}
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<Config>();
// 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
}

80
test-template.yaml Normal file
View file

@ -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 }}