Include plugin scaffolder actions directly in src
Signed-off-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
This commit is contained in:
parent
4b61eaef59
commit
a8a3026cb1
9 changed files with 460 additions and 1 deletions
17
examples/k8s-apply/skeleton/cm.yaml
Normal file
17
examples/k8s-apply/skeleton/cm.yaml
Normal 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
|
41
examples/k8s-apply/template-manifest-object.yaml
Normal file
41
examples/k8s-apply/template-manifest-object.yaml
Normal 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
|
41
examples/k8s-apply/template-manifest-string.yaml
Normal file
41
examples/k8s-apply/template-manifest-string.yaml
Normal 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
|
30
examples/k8s-apply/template.yaml
Normal file
30
examples/k8s-apply/template.yaml
Normal 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
|
|
@ -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": [
|
||||
|
|
186
packages/backend/src/plugins/k8s-apply.ts
Normal file
186
packages/backend/src/plugins/k8s-apply.ts
Normal 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];
|
||||
}
|
68
packages/backend/src/plugins/sanitize.ts
Normal file
68
packages/backend/src/plugins/sanitize.ts
Normal 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;
|
||||
}
|
|
@ -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 = [
|
||||
|
|
67
packages/backend/src/plugins/verify.ts
Normal file
67
packages/backend/src/plugins/verify.ts
Normal 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)
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
Loading…
Reference in a new issue