use @cnoe-io packages
This commit is contained in:
parent
91d2ccbb95
commit
e83e3d5068
11 changed files with 502 additions and 932 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -27,6 +27,9 @@ node_modules/
|
|||
# Node version directives
|
||||
.nvmrc
|
||||
|
||||
# NPM config files
|
||||
.npmrc
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
|
|
@ -45,8 +45,9 @@
|
|||
"@backstage/plugin-techdocs-react": "^1.1.6",
|
||||
"@backstage/plugin-user-settings": "^0.7.3",
|
||||
"@backstage/theme": "^0.3.0",
|
||||
"@cnoe-io/plugin-apache-spark": "file:/var/folders/b7/h6wzrfwn6l30pn3fk5j2794dcy0vlz/T/tmp-26390-66rLxROMRq6K",
|
||||
"@cnoe-io/plugin-argo-workflows": "file:/var/folders/b7/h6wzrfwn6l30pn3fk5j2794dcy0vlz/T/tmp-32426-R2tjIfGLJy55",
|
||||
"@cnoe-io/plugin-apache-spark": "0.1.0",
|
||||
"@cnoe-io/plugin-argo-workflows": "0.1.0",
|
||||
"@cnoe-io/plugin-scaffolder-actions-frontend": "0.1.0",
|
||||
"@internal/plugin-workflows": "^0.1.0",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
|
|
@ -38,7 +38,7 @@ import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
|
|||
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
|
||||
import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
|
||||
import { KubernetesClusterPickerExtension } from './scaffolder/kubernetesClusterPicker';
|
||||
import { KubernetesClusterPickerExtension } from '@cnoe-io/plugin-scaffolder-actions-frontend';
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
kubernetesApiRef,
|
||||
kubernetesAuthProvidersApiRef,
|
||||
} from '@backstage/plugin-kubernetes';
|
||||
import { FormHelperText } from '@material-ui/core';
|
||||
import { Progress, Select } from '@backstage/core-components';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { useTemplateSecrets } from '@backstage/plugin-scaffolder-react';
|
||||
import { ClusterPickerProps } from './schema';
|
||||
|
||||
export const KubernetesClusterPicker = (props: ClusterPickerProps) => {
|
||||
const k8sApi = useApi(kubernetesApiRef);
|
||||
const k8sAuthApi = useApi(kubernetesAuthProvidersApiRef);
|
||||
const { setSecrets } = useTemplateSecrets();
|
||||
const { uiSchema, required, onChange } = props;
|
||||
const { rawErrors } = props ?? [];
|
||||
const allowedClusters = useMemo(
|
||||
() => uiSchema?.['ui:options']?.allowedClusters ?? [],
|
||||
[uiSchema],
|
||||
);
|
||||
|
||||
const getToken = useCallback(
|
||||
async (clusterName: string) => {
|
||||
const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {};
|
||||
if (!requestUserCredentials) {
|
||||
return;
|
||||
}
|
||||
const cs = await k8sApi.getClusters();
|
||||
const cluster = cs.find(c => {
|
||||
return c.name === clusterName;
|
||||
});
|
||||
const { token } = await k8sAuthApi.getCredentials(cluster?.authProvider!);
|
||||
if (token === undefined) {
|
||||
return;
|
||||
}
|
||||
setSecrets({ [requestUserCredentials.secretKey]: token });
|
||||
},
|
||||
[k8sApi, k8sAuthApi, setSecrets, uiSchema],
|
||||
);
|
||||
|
||||
const { value: { clusters } = { clusters: [] }, loading } = useAsync(
|
||||
async () => {
|
||||
const c = await k8sApi.getClusters();
|
||||
const filteredClusters = c
|
||||
.filter(i => {
|
||||
if (allowedClusters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return allowedClusters.includes(i.name);
|
||||
})
|
||||
.map(i => ({ label: i.name, value: i.name }));
|
||||
if (filteredClusters.length) {
|
||||
await getToken(filteredClusters[0].value);
|
||||
onChange(filteredClusters[0].value);
|
||||
}
|
||||
return {
|
||||
clusters: filteredClusters,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required={required}
|
||||
error={rawErrors?.length > 0}
|
||||
>
|
||||
<Select
|
||||
native
|
||||
label="Cluster"
|
||||
items={clusters}
|
||||
onChange={e => {
|
||||
onChange(e.toString());
|
||||
getToken(e.toString());
|
||||
}}
|
||||
/>
|
||||
<FormHelperText>Kubernetes Cluster Name</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
|
@ -1,12 +0,0 @@
|
|||
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';
|
||||
import { KubernetesClusterPicker } from './KubernetesClusterPicker';
|
||||
import { ClusterPickerSchema } from './schema';
|
||||
|
||||
export const KubernetesClusterPickerExtension = scaffolderPlugin.provide(
|
||||
createScaffolderFieldExtension({
|
||||
name: 'KubernetesClusterPicker',
|
||||
component: KubernetesClusterPicker,
|
||||
schema: ClusterPickerSchema,
|
||||
}),
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
export { KubernetesClusterPickerExtension } from './extensions';
|
|
@ -1,31 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
|
||||
|
||||
export const ClusterPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
z.object({
|
||||
requestUserCredentials: z
|
||||
.object({
|
||||
secretKey: z
|
||||
.string()
|
||||
.describe(
|
||||
'Key used within the template secrets context to store the credential',
|
||||
),
|
||||
})
|
||||
.optional()
|
||||
.describe(
|
||||
'If defined will request user credentials to auth against the cluster',
|
||||
),
|
||||
allowedClusters: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('List of allowed Kubernetes clusters'),
|
||||
}),
|
||||
);
|
||||
|
||||
export const ClusterPickerSchema = ClusterPickerFieldSchema.schema;
|
||||
|
||||
export type ClusterPickerProps = typeof ClusterPickerFieldSchema.type;
|
||||
|
||||
export type ClusterPickerUiOptions =
|
||||
typeof ClusterPickerFieldSchema.uiOptionsType;
|
|
@ -37,6 +37,7 @@
|
|||
"@backstage/plugin-search-backend-node": "^1.2.1",
|
||||
"@backstage/plugin-techdocs-backend": "^1.6.2",
|
||||
"@backstage/types": "^1.1.0",
|
||||
"@cnoe-io/plugin-scaffolder-actions": "0.1.0",
|
||||
"@kubernetes/client-node": "^0.18.1",
|
||||
"@roadiehq/scaffolder-backend-module-utils": "^1.8.7",
|
||||
"app": "link:../app",
|
||||
|
|
|
@ -1,182 +0,0 @@
|
|||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { KubeConfig, CustomObjectsApi } from '@kubernetes/client-node';
|
||||
import YAML from 'yaml';
|
||||
import { Config } from '@backstage/config';
|
||||
import { resolveSafeChildPath } from '@backstage/backend-common';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export const kubernetesApply = (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',
|
||||
},
|
||||
manifestObject: {
|
||||
type: 'object',
|
||||
title: 'Manifest',
|
||||
description: 'The manifest to apply in the cluster',
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
let obj: any;
|
||||
if (ctx.input.manifestString) {
|
||||
obj = YAML.parse(ctx.input.manifestString);
|
||||
} else if (ctx.input.manifestObject) {
|
||||
obj = ctx.input.manifestObject;
|
||||
} else {
|
||||
const filePath = resolveSafeChildPath(
|
||||
ctx.workspacePath,
|
||||
ctx.input.manifestPath!,
|
||||
);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
obj = YAML.parse(fileContent);
|
||||
}
|
||||
const words = obj.apiVersion.split('/');
|
||||
const group = words[0];
|
||||
const version = words[1];
|
||||
// hack. needs fixing to correctly extract the plurals
|
||||
const plural = `${obj.kind.toLowerCase()}s`;
|
||||
const targetCluster = getClusterConfig(ctx.input.clusterName!, config);
|
||||
// hack. needs fixing to get the KubeConfig info from app-config.yaml
|
||||
const kc = new 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: 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(CustomObjectsApi);
|
||||
// Server-side apply.
|
||||
if (ctx.input.namespaced) {
|
||||
await client
|
||||
.patchNamespacedCustomObject(
|
||||
group,
|
||||
version,
|
||||
obj.metadata.namespace,
|
||||
plural,
|
||||
obj.metadata.name,
|
||||
obj,
|
||||
undefined,
|
||||
'backstage',
|
||||
true,
|
||||
{ headers: { 'Content-Type': 'application/apply-patch+yaml' } },
|
||||
)
|
||||
.then(
|
||||
resp => {
|
||||
ctx.logger.info(
|
||||
`Successfully created ${obj.metadata.namespace}/${obj.metadata.name} Application: HTTP ${resp.response.statusCode}`,
|
||||
);
|
||||
},
|
||||
err => {
|
||||
ctx.logger.error(
|
||||
`Failed to make PATCH call for ${obj.metadata.namespace}/${
|
||||
obj.metadata.name
|
||||
} Application: Body ${JSON.stringify(
|
||||
err.body,
|
||||
null,
|
||||
2,
|
||||
)} Response ${JSON.stringify(err.response, null, 2)}.`,
|
||||
);
|
||||
throw err;
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
await client
|
||||
.patchClusterCustomObject(
|
||||
group,
|
||||
version,
|
||||
plural,
|
||||
obj.metadata.name,
|
||||
obj,
|
||||
undefined,
|
||||
'backstage',
|
||||
true,
|
||||
{ headers: { 'Content-Type': 'application/apply-patch+yaml' } },
|
||||
)
|
||||
.then(
|
||||
resp => {
|
||||
ctx.logger.info(
|
||||
`Successfully created ${obj.metadata.name} Application: HTTP ${resp.response.statusCode}`,
|
||||
);
|
||||
},
|
||||
err => {
|
||||
ctx.logger.error(
|
||||
`Failed to make PATCH call for ${
|
||||
obj.metadata.name
|
||||
} Application: Body ${JSON.stringify(
|
||||
err.body,
|
||||
null,
|
||||
2,
|
||||
)} Response ${JSON.stringify(err.response, null, 2)}.`,
|
||||
);
|
||||
throw err;
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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];
|
||||
}
|
|
@ -21,7 +21,11 @@ import {
|
|||
createYamlJSONataTransformAction,
|
||||
createJsonJSONataTransformAction,
|
||||
} from '@roadiehq/scaffolder-backend-module-utils';
|
||||
import { kubernetesApply } from './kubernetes-apply';
|
||||
import {
|
||||
createKubernetesApply,
|
||||
createSanitizeResource,
|
||||
createVerifyDependency,
|
||||
} from '@cnoe-io/plugin-scaffolder-actions';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
|
@ -53,11 +57,17 @@ export default async function createPlugin(
|
|||
createJsonJSONataTransformAction(),
|
||||
];
|
||||
|
||||
const cnoeActions = [
|
||||
createSanitizeResource(),
|
||||
createVerifyDependency(),
|
||||
createKubernetesApply(env.config),
|
||||
];
|
||||
|
||||
const actions = [
|
||||
...builtInActions,
|
||||
...scaffolderBackendModuleUtils,
|
||||
...cnoeActions,
|
||||
createInvokeArgoAction(env.config, env.logger),
|
||||
kubernetesApply(env.config),
|
||||
];
|
||||
|
||||
return await createRouter({
|
||||
|
|
Loading…
Reference in a new issue