Include plugin scaffolder actions directly in src

Signed-off-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
This commit is contained in:
Manabu McCloskey 2024-03-07 11:26:01 -08:00 committed by Jesse Sanford
parent 4b61eaef59
commit a8a3026cb1
No known key found for this signature in database
GPG key ID: 1254665FB6385552
9 changed files with 460 additions and 1 deletions

View file

@ -0,0 +1,17 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: game-demo
data:
# property-like keys; each key maps to a simple value
player_initial_lives: "3"
ui_properties_file_name: "user-interface.properties"
# file-like keys
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true

View file

@ -0,0 +1,41 @@
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: deploy-resources-object
title: Deploy Resources using object
description: Deploy Resource to Kubernetes
spec:
owner: guest
type: service
# these are the steps which are rendered in the frontend with the form input
parameters: []
steps:
- id: template
name: Generating component
action: fetch:template
input:
url: ./skeleton
- id: apply
name: apply-manifest
action: cnoe:kubernetes:apply
input:
namespaced: true
manifestObject:
apiVersion: v1
kind: ConfigMap
metadata:
name: game-demo
data:
# property-like keys; each key maps to a simple value
player_initial_lives: "3"
ui_properties_file_name: "user-interface.properties"
# file-like keys
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
clusterName: local

View file

@ -0,0 +1,41 @@
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: deploy-resources-string
title: Deploy Resources using literal string
description: Deploy Resource to Kubernetes
spec:
owner: guest
type: service
# these are the steps which are rendered in the frontend with the form input
parameters: []
steps:
- id: template
name: Generating component
action: fetch:template
input:
url: ./skeleton
- id: apply
name: apply-manifest
action: cnoe:kubernetes:apply
input:
namespaced: true
manifestString: |
apiVersion: v1
kind: ConfigMap
metadata:
name: game-demo
data:
# property-like keys; each key maps to a simple value
player_initial_lives: "3"
ui_properties_file_name: "user-interface.properties"
# file-like keys
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
clusterName: local

View file

@ -0,0 +1,30 @@
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: deploy-resources
title: Deploy Resources
description: Deploy Resource to Kubernetes
spec:
owner: guest
type: service
# these are the steps which are rendered in the frontend with the form input
parameters:
- title: file name
properties:
path:
type: string
description: file name
default: cm.yaml
steps:
- id: template
name: Generating component
action: fetch:template
input:
url: ./skeleton
- id: apply
name: apply-manifest
action: cnoe:kubernetes:apply
input:
namespaced: true
manifestPath: cm.yaml
clusterName: local

View file

@ -41,6 +41,7 @@
"@backstage/plugin-search-backend-node": "~1.2.13",
"@backstage/plugin-techdocs-backend": "~1.9.2",
"@backstage/types": "~1.1.1",
"@kubernetes/client-node": "~0.20.0",
"@roadiehq/backstage-plugin-argo-cd-backend": "~2.14.0",
"@roadiehq/scaffolder-backend-module-utils": "~1.13.1",
"app": "link:../app",
@ -48,6 +49,7 @@
"dockerode": "^3.3.1",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"fs-extra": "~11.2.0",
"node-gyp": "^9.0.0",
"pg": "^8.11.3",
"winston": "^3.2.1"
@ -57,6 +59,7 @@
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/fs-extra": "^11.0.4",
"@types/luxon": "^2.0.4"
},
"files": [

View file

@ -0,0 +1,186 @@
import { createTemplateAction, executeShellCommand} from '@backstage/plugin-scaffolder-node';
import { dumpYaml } from '@kubernetes/client-node';
import yaml from 'js-yaml';
import { Config } from '@backstage/config';
import { resolveSafeChildPath } from '@backstage/backend-common';
import fs from 'fs-extra';
export const createKubernetesApply = (config: Config) => {
return createTemplateAction<{
manifestString?: string;
manifestObject?: any;
manifestPath?: string;
namespaced: boolean;
clusterName?: string;
}>({
id: 'cnoe:kubernetes:apply',
schema: {
input: {
type: 'object',
required: ['namespaced'],
properties: {
manifestString: {
type: 'string',
title: 'Manifest',
description:
'The manifest to apply in the cluster. Must be a string',
},
manifestObject: {
type: 'object',
title: 'Manifest',
description:
'The manifest to apply in the cluster. Must be an object',
},
manifestPath: {
type: 'string',
title: 'Path to the manifest file',
description: 'The path to the manifest file.',
},
namespaced: {
type: 'boolean',
title: 'Namespaced',
description: 'Whether the API is namespaced or not',
},
clusterName: {
type: 'string',
title: 'Cluster Name',
description: 'The name of the cluster to apply this',
},
},
},
output: {
type: 'object',
title: 'Returned object',
description:
'The object returned by Kubernetes by performing this operation',
},
},
async handler(ctx) {
let obj: any;
let manifestPath = resolveSafeChildPath(ctx.workspacePath, 'to-be-applied.yaml');
if (ctx.input.manifestString) {
obj = yaml.load(ctx.input.manifestString)
fs.writeFileSync(manifestPath, ctx.input.manifestString, {
encoding: 'utf8',
mode: '600',
});
} else if (ctx.input.manifestObject) {
obj = ctx.input.manifestObject;
fs.writeFileSync(manifestPath, yaml.dump(ctx.input.manifestObject), {
encoding: 'utf8',
mode: '600',
});
} else {
const filePath = resolveSafeChildPath(
ctx.workspacePath,
ctx.input.manifestPath!,
);
const fileContent = fs.readFileSync(filePath, 'utf8');
manifestPath = filePath
obj = yaml.load(fileContent);
}
if (ctx.input.clusterName) {
// Supports SA token authentication only
const targetCluster = getClusterConfig(ctx.input.clusterName!, config);
const confFile = {
apiVersion: 'v1',
kind: 'Config',
'current-context': ctx.input.clusterName,
contexts: [
{
name: ctx.input.clusterName,
context: {
cluster: ctx.input.clusterName,
user: ctx.input.clusterName,
},
},
],
clusters: [
{
name: ctx.input.clusterName,
cluster: {
'certificate-authority-data': targetCluster.getOptionalString('caData'),
'certificate-authority': targetCluster.getOptionalString('caFile'),
server: targetCluster.getString('url'),
'insecure-skip-tls-verify': !!targetCluster.getOptionalBoolean('skipTLSVerify'),
},
},
],
users: [
{
name: ctx.input.clusterName,
user: {
token: targetCluster.getString('serviceAccountToken'),
},
},
],
};
if (!confFile.clusters[0].cluster["insecure-skip-tls-verify"]) {
let caDataRaw = targetCluster.getOptionalString('caData')
if (caDataRaw?.startsWith('-----BEGIN CERTIFICATE-----')) {
caDataRaw = Buffer.from(targetCluster.getString('caData'), 'utf8').toString(
'base64',
);
}
confFile.clusters[0].cluster['certificate-authority-data'] = caDataRaw
}
const confString = dumpYaml(confFile);
const confFilePath = resolveSafeChildPath(ctx.workspacePath, 'config');
fs.writeFileSync(confFilePath, confString, {
encoding: 'utf8',
mode: '600',
});
await executeShellCommand({
command: 'cat',
args: [confFilePath],
logStream: ctx.logStream,
});
await executeShellCommand({
command: 'cat',
args: [manifestPath],
logStream: ctx.logStream,
});
if (obj.metadata.generateName !== undefined) {
await executeShellCommand({
command: 'kubectl',
args: ['--kubeconfig', confFilePath, 'create', '-f', manifestPath],
logStream: ctx.logStream,
});
return;
}
await executeShellCommand({
command: 'kubectl',
args: ['--kubeconfig', confFilePath, 'apply', '-f', manifestPath],
logStream: ctx.logStream,
});
return;
}
throw new Error("please specify a valid cluster name")
},
});
};
// Finds the first cluster that matches the given name.
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<Config>();
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];
}

View file

@ -0,0 +1,68 @@
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import yaml from 'js-yaml';
// Add type annotations to fix TS2742
type SanitizeResourceInput = {
document: string;
};
type SanitizeResourceOutput = {
sanitized: string;
};
export const createSanitizeResource = () => {
return createTemplateAction<SanitizeResourceInput, SanitizeResourceOutput>({
id: 'cnoe:utils:sanitize',
schema: {
input: {
type: 'object',
required: ['document'],
properties: {
document: {
type: 'string',
title: 'Document',
description: 'The document to be sanitized',
},
},
},
output: {
type: 'object',
properties: {
sanitized: {
type: 'string',
description: 'The sanitized yaml string'
}
}
}
},
async handler(ctx) {
const obj = yaml.load(ctx.input.document);
ctx.output('sanitized', yaml.dump(removeEmptyObjects(obj)));
},
});
};
// Remove empty elements from an object
function removeEmptyObjects(obj: any): any {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const newObj: any = Array.isArray(obj) ? [] : {};
for (const key in obj) {
const value = obj[key];
const newValue = removeEmptyObjects(value);
if (
!(
newValue === null ||
newValue === undefined ||
(typeof newValue === 'object' && Object.keys(newValue).length === 0)
)
) {
newObj[key] = newValue;
}
}
return newObj;
}

View file

@ -21,6 +21,9 @@ import {
createJsonJSONataTransformAction,
createReplaceInFileAction
} from '@roadiehq/scaffolder-backend-module-utils';
import {createKubernetesApply} from "./k8s-apply";
import {createSanitizeResource} from "./sanitize";
import {createVerifyDependency} from "./verify";
export default async function createPlugin(
env: PluginEnvironment,
@ -48,7 +51,10 @@ export default async function createPlugin(
const cnoeActions = [
createPublishGiteaAction(options),
createArgoCDApp(argocdOptions)
createArgoCDApp(argocdOptions),
createKubernetesApply(env.config),
createSanitizeResource(),
createVerifyDependency()
]
const roadieUtilActions = [

View file

@ -0,0 +1,67 @@
import { executeShellCommand } from '@backstage/plugin-scaffolder-node';
import { createTemplateAction }from '@backstage/plugin-scaffolder-node';
import {Writable} from 'stream';
class ConsoleLogStream extends Writable {
data: string;
constructor(options: any) {
super(options);
this.data = '';
}
_write(chunk: any, _: any, callback: any) {
this.data += chunk.toString(); // Convert the chunk to a string and append it to this.data
console.log(this.data)
callback();
}
}
export const createVerifyDependency = () => {
return createTemplateAction<{
verifiers: string[];
}>({
id: 'cnoe:verify:dependency',
schema: {
input: {
type: 'object',
required: ['verifiers'],
properties: {
verifiers: {
type: 'array',
items: {
type: 'string',
},
title: 'verifiers',
description: 'The list of verifiers',
},
},
},
},
async handler(ctx) {
const verifiers = ctx.input.verifiers
if (verifiers === null || verifiers.length === 0) {
ctx.logger.error('no verifier was supplied for the object')
return
}
const baseCommand = 'cnoe'
const baseArguments = ['k8s', 'verify']
verifiers.forEach((verifier: string) => baseArguments.push("--config", verifier))
const logStream = new ConsoleLogStream({});
await executeShellCommand({
command: baseCommand,
args: baseArguments,
logStream: logStream,
}).then(() =>
ctx.logger.info("verification succeeded")
).catch((error) => {
ctx.logger.error(error)
throw new Error(logStream.data)
});
},
});
};