use @cnoe-io packages

This commit is contained in:
Manabu Mccloskey 2023-07-27 12:34:20 -07:00
parent 91d2ccbb95
commit e83e3d5068
11 changed files with 502 additions and 932 deletions

3
.gitignore vendored
View file

@ -27,6 +27,9 @@ node_modules/
# Node version directives
.nvmrc
# NPM config files
.npmrc
# dotenv environment variables file
.env
.env.test

View file

@ -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",

View file

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

View file

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

View file

@ -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,
}),
);

View file

@ -1 +0,0 @@
export { KubernetesClusterPickerExtension } from './extensions';

View file

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

View file

@ -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",

View file

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

View file

@ -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({

1096
yarn.lock

File diff suppressed because it is too large Load diff