feat(odo): create odo workspace and plugins
Signed-off-by: Paul Schultz <pschultz@pobox.com>
This commit is contained in:
parent
ca0e201e1e
commit
bcfbc5c7c0
|
|
@ -0,0 +1,8 @@
|
|||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.git
|
||||
.yarn/cache
|
||||
.yarn/install-state.gz
|
||||
node_modules
|
||||
packages/*/src
|
||||
packages/*/node_modules
|
||||
plugins
|
||||
*.local.yaml
|
||||
|
|
@ -0,0 +1 @@
|
|||
playwright.config.ts
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
};
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Coverage directory generated when running tests with coverage
|
||||
coverage
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Yarn 3 files
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Node version directives
|
||||
.nvmrc
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# Build output
|
||||
dist
|
||||
dist-types
|
||||
|
||||
# Temporary change files created by Vim
|
||||
*.swp
|
||||
|
||||
# MkDocs build output
|
||||
site
|
||||
|
||||
# Local configuration files
|
||||
*.local.yaml
|
||||
|
||||
# Sensitive credentials
|
||||
*-credentials.yaml
|
||||
|
||||
# vscode database functionality support files
|
||||
*.session.sql
|
||||
|
||||
# E2E test reports
|
||||
e2e-test-report/
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
dist
|
||||
dist-types
|
||||
coverage
|
||||
.vscode
|
||||
.eslintrc.js
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# backstage-odo-devfile-plugin
|
||||
|
||||
[odo](https://odo.dev/) and [Devfile](https://devfile.io/) integration within Backstage.
|
||||
|
||||
This repository contains a set of extensions for Backstage that can be used to write your own [Software Templates](https://backstage.io/docs/features/software-templates/):
|
||||
|
||||
- [devfile-field-extension](plugins/devfile-field-extension): a [Custom Field Extension](https://backstage.io/docs/features/software-templates/writing-custom-field-extensions/) that allows you to add a set of drop-down lists to pick a Devfile Stack version, a version, and a starter project.
|
||||
|
||||
- [scaffolder-odo-actions-backend](plugins/scaffolder-odo-actions-backend): a [Backend Plugin](https://backstage.io/docs/plugins/backend-plugin/) containing a set of [Custom Actions](https://backstage.io/docs/features/software-templates/writing-custom-actions) using the [`odo`](https://odo.dev/) CLI.
|
||||
|
||||
## Example
|
||||
|
||||
See <https://github.com/ododev/odo-backstage-software-template> for an end-to-end example of a [Backstage Software Template](https://backstage.io/docs/features/software-templates/) relying on the extensions above.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"version": "1.28.4"
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: odo
|
||||
description: An example of a Backstage application.
|
||||
# Example for optional annotations
|
||||
# annotations:
|
||||
# github.com/project-slug: backstage/backstage
|
||||
# backstage.io/techdocs-ref: dir:.
|
||||
spec:
|
||||
type: website
|
||||
owner: john@example.com
|
||||
lifecycle: experimental
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "@internal/odo",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "18 || 20"
|
||||
},
|
||||
"scripts": {
|
||||
"tsc": "tsc",
|
||||
"tsc:full": "tsc --skipLibCheck false --incremental false",
|
||||
"build:all": "backstage-cli repo build --all",
|
||||
"build:api-reports": "yarn build:api-reports:only --tsc",
|
||||
"build:api-reports:only": "backstage-repo-tools api-reports -o ae-wrong-input-file-type --validate-release-tags",
|
||||
"clean": "backstage-cli repo clean",
|
||||
"test": "backstage-cli repo test",
|
||||
"test:all": "backstage-cli repo test --coverage",
|
||||
"fix": "backstage-cli repo fix",
|
||||
"lint": "backstage-cli repo lint --since origin/main",
|
||||
"lint:all": "backstage-cli repo lint",
|
||||
"prettier:check": "prettier --check .",
|
||||
"new": "backstage-cli new --scope @backstage-community"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"plugins/*"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/community-plugins",
|
||||
"directory": "workspaces/odo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.26.7",
|
||||
"@backstage/e2e-test-utils": "^0.1.1",
|
||||
"@backstage/repo-tools": "^0.8.0",
|
||||
"@changesets/cli": "^2.27.1",
|
||||
"@spotify/prettier-config": "^12.0.0",
|
||||
"node-gyp": "^9.0.0",
|
||||
"prettier": "^2.3.2",
|
||||
"typescript": "~5.3.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18"
|
||||
},
|
||||
"prettier": "@spotify/prettier-config",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,mjs,cjs}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# devfile-field-extension
|
||||
|
||||
This plugin is a Custom Field Extension for Backstage. It allows you to add a set of drop-down lists to pick a Devfile Stack version, a version, and a starter project.
|
||||
It interacts with the configured [Devfile registry](https://registry.devfile.io/viewer) to load the list of available Devfile Stacks.
|
||||
|
||||
## Preview
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
From your Backstage instance root folder:
|
||||
|
||||
```shell
|
||||
yarn add --cwd packages/app @backstage-community/plugin-odo-module-devfile-field-extension
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Add the import to your `packages/app/src/App.tsx` on the frontend package of your Backstage instance:
|
||||
|
||||
```tsx
|
||||
// packages/app/src/App.tsx
|
||||
|
||||
import { DevfileSelectorFieldExtension } from '@backstage-community/plugin-odo-module-devfile-field-extension';
|
||||
import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';
|
||||
```
|
||||
|
||||
2. Then add the imported field extension as a child of `ScaffolderFieldExtensions` inside `Route`, like so:
|
||||
|
||||
```tsx
|
||||
// packages/app/src/App.tsx
|
||||
|
||||
<Route path="/create" element={<ScaffolderPage />}>
|
||||
<ScaffolderFieldExtensions>
|
||||
<DevfileSelectorFieldExtension />
|
||||
</ScaffolderFieldExtensions>
|
||||
</Route>
|
||||
```
|
||||
|
||||
3. This custom field extension populates the dropdown lists from a Devfile Registry, which is expected to be proxied by Backstage at the following path: `/devfile-registry`. As such, you need to add the appropriate configuration to your `app-config.yaml` file under the `proxy.endpoints` field, like so:
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
|
||||
proxy:
|
||||
endpoints:
|
||||
'/devfile-registry':
|
||||
target: 'https://registry.devfile.io'
|
||||
```
|
||||
|
||||
You should now see the custom field `DevfileSelectorExtension` and its dropdown lists populated if you navigate to the Template Editor page at <http://localhost:3000/create/edit>.
|
||||
|
||||
## Usage
|
||||
|
||||
To use the extension in a Backstage Software Template, you can add the `ui:field` field to the parameter. The output of the extension is an object made up of the following fields:
|
||||
- `devfile`: the Devfile Stack selected by the user.
|
||||
- `version`: the Devfile Stack version selected by the user
|
||||
- `starter_project`: the Devfile Starter Project selected by the user. Optional.
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- title: Provide details about the Devfile
|
||||
required:
|
||||
- devfile_data
|
||||
properties:
|
||||
devfile_data:
|
||||
type: object
|
||||
required:
|
||||
- devfile
|
||||
- version
|
||||
properties:
|
||||
devfile:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
starter_project:
|
||||
type: string
|
||||
ui:field: DevfileSelectorExtension
|
||||
ui:autofocus: true
|
||||
ui:options:
|
||||
rows: 5
|
||||
```
|
||||
|
||||
See <https://github.com/ododev/odo-backstage-software-template> for an example of a [Software Template](https://backstage.io/docs/features/software-templates/) making use of the Devfile Selector Field Extension.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { devfileSelectorExtensionPlugin, DevfileSelectorFieldExtension } from '../src/plugin';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(devfileSelectorExtensionPlugin)
|
||||
.addPage({
|
||||
element: <DevfileSelectorFieldExtension />,
|
||||
})
|
||||
.render();
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"name": "@backstage-community/plugin-odo-module-devfile-field-extension",
|
||||
"version": "0.21.0",
|
||||
"description": "The devfile modules for odo",
|
||||
"author": "Red Hat",
|
||||
"homepage": "https://odo.dev",
|
||||
"backstage": {
|
||||
"role": "frontend-plugin"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/community-plugins",
|
||||
"directory": "workspaces/odo/plugins/odo-module-devfile-field-extension"
|
||||
},
|
||||
"main": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core-plugin-api": "^1.9.3",
|
||||
"@backstage/plugin-scaffolder": "^1.22.0",
|
||||
"@backstage/plugin-scaffolder-react": "^1.9.0",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"react-use": "^17.5.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.26.10",
|
||||
"@backstage/dev-utils": "^1.0.34"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"package.json",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
import React, { useState } from "react";
|
||||
import { FormControl, TextField } from "@material-ui/core";
|
||||
import { z } from "zod";
|
||||
import { makeFieldSchemaFromZod } from "@backstage/plugin-scaffolder";
|
||||
import { useAsync } from "react-use";
|
||||
import Autocomplete from "@material-ui/lab/Autocomplete";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { useApi, configApiRef } from "@backstage/core-plugin-api";
|
||||
|
||||
const DevfileSelectorExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod(
|
||||
z.object({
|
||||
devfile: z.string().describe("Devfile name"),
|
||||
version: z.string().describe("Devfile Stack version"),
|
||||
starter_project: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Devfile Stack starter project"),
|
||||
})
|
||||
);
|
||||
|
||||
export const DevfileSelectorExtensionWithOptionsSchema =
|
||||
DevfileSelectorExtensionWithOptionsFieldSchema.schema;
|
||||
|
||||
type DevfileSelectorExtensionWithOptionsProps =
|
||||
typeof DevfileSelectorExtensionWithOptionsFieldSchema.type;
|
||||
|
||||
export interface DevfileStack {
|
||||
name: string;
|
||||
displayName: string | undefined;
|
||||
icon: string;
|
||||
versions: DevfileStackVersion[];
|
||||
}
|
||||
|
||||
export interface DevfileStackVersion {
|
||||
version: string;
|
||||
starterProjects: string[];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
option: {
|
||||
fontSize: 15,
|
||||
"& > span": {
|
||||
marginRight: 10,
|
||||
fontSize: 18,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const DevfileSelectorExtension = ({
|
||||
onChange,
|
||||
rawErrors,
|
||||
required,
|
||||
formData,
|
||||
idSchema,
|
||||
schema: { description },
|
||||
}: DevfileSelectorExtensionWithOptionsProps) => {
|
||||
const config = useApi(configApiRef);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState<DevfileStack[]>([]);
|
||||
const [selectedStack, setSelectedStack] = useState("");
|
||||
const [versions, setVersions] = useState<string[]>([]);
|
||||
const [selectedVersion, setSelectedVersion] = useState("");
|
||||
const [starterprojects, setStarterprojects] = useState<string[]>([]);
|
||||
|
||||
const backendUrl = config.getString("backend.baseUrl");
|
||||
// This requires a proxy endpoint to be added for /devfile-registry
|
||||
const registryApiEndpoint = `${backendUrl}/api/proxy/devfile-registry/v2index`;
|
||||
|
||||
useAsync(async () => {
|
||||
const req = await fetch(registryApiEndpoint, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
const resp = (await req.json()) as DevfileStack[];
|
||||
resp.sort((a, b) =>
|
||||
(a.displayName ?? "").localeCompare(b.displayName ?? "")
|
||||
);
|
||||
setData(resp);
|
||||
|
||||
setVersions([]);
|
||||
setStarterprojects([]);
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
const handleDevfileStack = (value: DevfileStack) => {
|
||||
const filteredStacks = data.filter((stack) => stack.name === value?.name);
|
||||
const versionList = filteredStacks.flatMap((stack) => stack.versions);
|
||||
const filteredVersions = versionList.map((v) => v.version);
|
||||
filteredVersions.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
let filteredStarterProjects: string[] = [];
|
||||
if (versionList.length > 0) {
|
||||
filteredStarterProjects = versionList[0].starterProjects ?? [];
|
||||
}
|
||||
|
||||
setSelectedStack(value.name);
|
||||
setSelectedVersion(filteredVersions?.length > 0 ? filteredVersions[0] : "");
|
||||
setVersions(filteredVersions);
|
||||
setStarterprojects(filteredStarterProjects);
|
||||
|
||||
onChange({
|
||||
devfile: value.name,
|
||||
version: versionList.length > 0 ? versionList[0].version : "",
|
||||
starter_project:
|
||||
filteredStarterProjects.length > 0 ? filteredStarterProjects[0] : "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleDevfileStackVersion = (value: any) => {
|
||||
const filteredResult = data
|
||||
.filter((stack) => stack.name === selectedStack)
|
||||
.flatMap((stack) => stack.versions)
|
||||
.filter((v) => v.version === value)
|
||||
.flatMap((v) => v.starterProjects ?? []);
|
||||
filteredResult.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
setSelectedVersion(value as string);
|
||||
setStarterprojects(filteredResult);
|
||||
|
||||
onChange({
|
||||
devfile: selectedStack,
|
||||
version: value as string,
|
||||
starter_project: filteredResult.length > 0 ? filteredResult[0] : "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleDevfileStarterProject = (value: any) => {
|
||||
onChange({
|
||||
devfile: selectedStack,
|
||||
version: selectedVersion,
|
||||
starter_project: value as string,
|
||||
});
|
||||
};
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required={required}
|
||||
error={rawErrors?.length > 0}
|
||||
>
|
||||
<div>
|
||||
<Autocomplete
|
||||
id={`devfile-selector-${idSchema?.$id}`}
|
||||
loading={loading}
|
||||
noOptionsText="No Devfile Stacks available from registry"
|
||||
value={
|
||||
// dummy DevfileStack object with the name set, so that getOptionSelected can resolve the right item from data
|
||||
{ name: formData?.devfile ?? selectedStack, icon: "", displayName: "", versions: [] }
|
||||
}
|
||||
classes={{
|
||||
option: classes.option,
|
||||
}}
|
||||
options={data}
|
||||
renderOption={(option) =>
|
||||
option.icon ? (
|
||||
<React.Fragment>
|
||||
<span>
|
||||
<img
|
||||
style={{ width: 50, height: 50 }}
|
||||
src={option.icon}
|
||||
alt={`icon for ${option.name}`}
|
||||
/>
|
||||
</span>
|
||||
{option.displayName}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>{option.displayName}</React.Fragment>
|
||||
)
|
||||
}
|
||||
getOptionLabel={(option) => option.name}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Devfile Stack"
|
||||
variant="outlined"
|
||||
required={required}
|
||||
error={rawErrors?.length > 0 && !formData}
|
||||
inputProps={{
|
||||
...params.inputProps,
|
||||
autoComplete: "new-password", // disable autocomplete and autofill
|
||||
}}
|
||||
helperText={description}
|
||||
/>
|
||||
)}
|
||||
onChange={(_, value) => handleDevfileStack(value)}
|
||||
getOptionSelected={(option, value) => option.name === value.name}
|
||||
disableClearable
|
||||
/>
|
||||
</div>
|
||||
<br/>
|
||||
<div>
|
||||
<Autocomplete
|
||||
id={`devfile-version-selector-${idSchema?.$id}`}
|
||||
loading={loading}
|
||||
value={
|
||||
formData?.version ?? selectedVersion ?? (versions.length > 0 ? versions[0] : null)
|
||||
}
|
||||
noOptionsText="No version available in Devfile Stack"
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Version"
|
||||
variant="outlined"
|
||||
required={required}
|
||||
error={rawErrors?.length > 0 && !formData}
|
||||
helperText={description}
|
||||
inputProps={{
|
||||
...params.inputProps,
|
||||
autoComplete: "new-password", // disable autocomplete and autofill
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
options={versions}
|
||||
onChange={(_, value) => handleDevfileStackVersion(value)}
|
||||
getOptionSelected={(option, value) => option === value}
|
||||
disableClearable
|
||||
/>
|
||||
</div>
|
||||
<br/>
|
||||
<div>
|
||||
<Autocomplete
|
||||
id={`devfile-starter-project-selector-${idSchema?.$id}`}
|
||||
loading={loading}
|
||||
value={
|
||||
formData?.starter_project ??
|
||||
(starterprojects.length > 0 ? starterprojects[0] : "")
|
||||
}
|
||||
noOptionsText="No starter project available in Devfile Stack"
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Starter Project"
|
||||
variant="outlined"
|
||||
required={false}
|
||||
error={rawErrors?.length > 0 && !formData}
|
||||
inputProps={{
|
||||
...params.inputProps,
|
||||
autoComplete: "new-password", // disable autocomplete and autofill
|
||||
}}
|
||||
helperText={description}
|
||||
/>
|
||||
)}
|
||||
options={starterprojects}
|
||||
onChange={(_, value) => handleDevfileStarterProject(value)}
|
||||
getOptionSelected={(option, value) => option === value}
|
||||
disableClearable
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export {
|
||||
devfileSelectorExtensionPlugin,
|
||||
DevfileSelectorFieldExtension,
|
||||
} from "./plugin";
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { devfileSelectorExtensionPlugin } from './plugin';
|
||||
|
||||
describe('plugin-scaffolder-frontend-module-devfile-field', () => {
|
||||
it('should export DevfileSelector plugin', () => {
|
||||
expect(devfileSelectorExtensionPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { createPlugin } from '@backstage/core-plugin-api';
|
||||
|
||||
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
|
||||
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
import { DevfileSelectorExtension, DevfileSelectorExtensionWithOptionsSchema } from './components/DevfileSelectorExtension';
|
||||
|
||||
export const devfileSelectorExtensionPlugin = createPlugin({
|
||||
id: 'devfile-selector-extension'
|
||||
})
|
||||
|
||||
export const DevfileSelectorFieldExtension = scaffolderPlugin.provide(
|
||||
createScaffolderFieldExtension({
|
||||
name: 'DevfileSelectorExtension',
|
||||
component: DevfileSelectorExtension,
|
||||
schema: DevfileSelectorExtensionWithOptionsSchema,
|
||||
}),
|
||||
);
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'plugin-scaffolder-frontend-module-devfile-field',
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
import '@testing-library/jest-dom';
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
"include": [
|
||||
"src",
|
||||
"dev"
|
||||
],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist-types",
|
||||
"rootDir": "."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
# scaffolder-odo-actions
|
||||
|
||||
This is a [Backend Plugin](https://backstage.io/docs/plugins/backend-plugin/) containing a set of [Custom Actions](https://backstage.io/docs/features/software-templates/writing-custom-actions) using the [`odo`](https://odo.dev/) CLI.
|
||||
It contains the following actions:
|
||||
|
||||
- `devfile:odo:command`: a generic action that can execute any `odo` command from the scaffolder workspace.
|
||||
|
||||
- `devfile:odo:component:init`: allows to execute the [`odo init`](https://odo.dev/docs/command-reference/init) command from the Scaffolder workspace. The goal of this action is to generate a starter project for a given Devfile that can be customized later on.
|
||||
|
||||
## Preview
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
From your Backstage instance root folder:
|
||||
|
||||
```shell
|
||||
yarn add --cwd packages/backend @backstage-community/plugin-scaffolder-backend-module-odo
|
||||
```
|
||||
|
||||
This will download the right `odo` binary for the current operating system and architecture from the Red Hat servers at <https://developers.redhat.com/content-gateway/rest/mirror/pub/openshift-v4/clients/odo/>.
|
||||
|
||||
This behavior can be customized by adding a new `"odo"` field in your `packages/backend/package.json` file, like so:
|
||||
|
||||
```json
|
||||
// packages/backend/package.json
|
||||
|
||||
{
|
||||
"odo": {
|
||||
// specifying the version is optional.
|
||||
// You can also specify "latest" to use the latest version of odo, or "nightly" to use the latest nightly build of odo.
|
||||
"version": "3.15.0",
|
||||
"skipDownload": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that the custom actions here do require an `odo` binary to work properly.
|
||||
So if you choose to skip the download (using the `odo.skipDownload` property above), you need to make sure to meet any of the requirements below:
|
||||
- either you can explicitly set the path to the `odo` binary in your `app-config.yaml` (see [below](#app-configyaml));
|
||||
- or `odo` is already [installed](https://odo.dev/docs/overview/installation) and available globally in the system paths of the environment the Backstage instance is running in.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Code
|
||||
|
||||
Import the module by modifying the `packages/backend/src/index.ts` file on your Backstage instance:
|
||||
|
||||
```ts
|
||||
// packages/backend/src/index.ts
|
||||
|
||||
backend.add(import('@backstage-community/plugin-scaffolder-backend-module-odo'))
|
||||
```
|
||||
|
||||
### app-config.yaml
|
||||
|
||||
Optionally, the behavior of these custom actions can be customized by adding the following section to your `app-config.yaml` file:
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
|
||||
odo:
|
||||
# When adding this plugin to your Backstage instance, it will automatically try to download the right odo binary and use it.
|
||||
# But if you already have odo installed, you can override the path below.
|
||||
# binaryPath: '/path/to/odo'
|
||||
telemetry:
|
||||
# Disable the odo telemetry.
|
||||
# Default: false
|
||||
disabled: false
|
||||
devfileRegistry:
|
||||
# Used for calling `odo init` and any other custom actions relying on a Devfile registry.
|
||||
# If you are using the Devfile Selector Custom Field Extension in your template,
|
||||
# you need to also add this URL to the 'proxy.endpoints' field under a '/devfile-registry' field.
|
||||
# Default: 'https://registry.devfile.io'
|
||||
url: 'https://registry.devfile.io'
|
||||
```
|
||||
|
||||
You should now see the custom `devfile:odo:*` actions if you navigate to the Actions page at <http://localhost:3000/create/actions>.
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
You can use the action in any of the steps of your [Software Template](https://backstage.io/docs/features/software-templates/).
|
||||
See <https://github.com/ododev/odo-backstage-software-template> for an example of a Software Template making use of the Actions here.
|
||||
|
||||
### Example with the `odo init` action
|
||||
|
||||
This action can be used in conjunction with the [odo-module-devfile-field-extension](../odo-module-devfile-field-extension) Custom Field Extension to get the Devfile input data from the end-user, e.g.:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
parameters:
|
||||
|
||||
- title: Provide details about the Devfile
|
||||
required:
|
||||
- devfile_data
|
||||
properties:
|
||||
devfile_data:
|
||||
type: object
|
||||
required:
|
||||
- devfile
|
||||
- version
|
||||
properties:
|
||||
devfile:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
starter_project:
|
||||
type: string
|
||||
ui:field: DevfileSelectorExtension
|
||||
|
||||
steps:
|
||||
- id: odo-init
|
||||
name: Generate
|
||||
action: devfile:odo:component:init
|
||||
input:
|
||||
name: ${{ parameters.name }}
|
||||
devfile: ${{ parameters.devfile_data.devfile }}
|
||||
version: ${{ parameters.devfile_data.version }}
|
||||
starter_project: ${{ parameters.devfile_data.starter_project }}
|
||||
```
|
||||
|
||||
### Example with the generic `odo` action
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
# [...]
|
||||
|
||||
steps:
|
||||
- id: generic-odo-command
|
||||
name: Execute odo command
|
||||
action: devfile:odo:command
|
||||
input:
|
||||
command: ${{ parameters.command }} # e.g.: 'analyze'
|
||||
args: ${{ parameters.args }} # e.g.: ['-o', 'json']
|
||||
```
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
export interface Config {
|
||||
odo: {
|
||||
/**
|
||||
* Path to the odo binary.
|
||||
* Note that when installing the custom actions, the latest version of odo is always downloaded.
|
||||
* But this config option allows to use a different binary if needed.
|
||||
* @visibility backend
|
||||
*/
|
||||
binaryPath: string | undefined;
|
||||
telemetry: {
|
||||
/**
|
||||
* odo telemetry status
|
||||
* @visibility backend
|
||||
*/
|
||||
disabled: boolean | undefined;
|
||||
},
|
||||
devfileRegistry: {
|
||||
/**
|
||||
* devfile registry URL
|
||||
* @visibility backend
|
||||
*/
|
||||
url: string | undefined;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"name": "@backstage-community/plugin-scaffolder-backend-module-odo",
|
||||
"version": "0.21.0",
|
||||
"description": "The odo module for @backstage/plugin-scaffolder-backend",
|
||||
"author": "Red Hat",
|
||||
"homepage": "https://odo.dev",
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module",
|
||||
"pluginId": "scaffolder",
|
||||
"pluginPackage": "@backstage/plugin-scaffolder-backend"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/community-plugins",
|
||||
"directory": "workspaces/odo/plugins/scaffolder-backend-module-odo"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"postinstall": "node scripts/post-install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "^0.6.21",
|
||||
"@backstage/plugin-scaffolder-node": "^0.4.7",
|
||||
"cachedir": "^2.4.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got": "^11.7.0",
|
||||
"gunzip-maybe": "^1.4.2",
|
||||
"hasha": "^5.2.2",
|
||||
"pkg-conf": "^3.1.0",
|
||||
"tar-fs": "^2.1.0",
|
||||
"unzip-stream": "^0.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.26.10",
|
||||
"@backstage/config": "^1.2.0",
|
||||
"@types/fs-extra": "^11.0.4"
|
||||
},
|
||||
"files": [
|
||||
"scripts",
|
||||
"dist",
|
||||
"package.json",
|
||||
"README.md",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
"use strict";
|
||||
|
||||
// Inspired from https://github.com/ipfs/npm-kubo
|
||||
|
||||
const goenv = require("./go-platform");
|
||||
const gunzip = require("gunzip-maybe");
|
||||
const got = require("got").default;
|
||||
const path = require("path");
|
||||
const tarFS = require("tar-fs");
|
||||
const unzip = require("unzip-stream");
|
||||
const pkgConf = require("pkg-conf");
|
||||
const cachedir = require("cachedir");
|
||||
const fs = require("fs");
|
||||
const hasha = require("hasha");
|
||||
|
||||
// Version of odo to install. This is known to be working with this plugin.
|
||||
// Can be overridden by clients either via the BACKSTAGE_ODO_PLUGIN__ODO_VERSION environment variable
|
||||
// or via the 'odo.version' field in their 'package.json' file.
|
||||
const ODO_VERSION = "3.15.0";
|
||||
const ODO_DIST_URL =
|
||||
"https://developers.redhat.com/content-gateway/rest/mirror/pub/openshift-v4/clients/odo";
|
||||
const ODO_DIST_URL_NIGHTLY = "https://s3.eu-de.cloud-object-storage.appdomain.cloud/odo-nightly-builds";
|
||||
|
||||
// Map of all architectures that can be donwloaded on the odo distribution URL.
|
||||
const SUPPORTED_ARCHITECTURES_BY_PLATFORM = new Map(
|
||||
Object.entries({
|
||||
darwin: ["amd64", "arm64"],
|
||||
linux: ["amd64", "arm64", "ppc64le", "s390x"],
|
||||
windows: ["amd64"],
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* This avoids an expensive download if file is already in cache.
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} platform
|
||||
* @param {string} arch
|
||||
* @param {string} version
|
||||
*/
|
||||
async function cachingFetchAndVerify(url, platform, arch, version) {
|
||||
const parentCacheDir = cachedir("odo");
|
||||
const cacheDir = path.join(parentCacheDir, version);
|
||||
const filename = url.split("/").pop();
|
||||
|
||||
if (!filename) {
|
||||
throw new Error(`Invalid URL: ${url}`);
|
||||
}
|
||||
|
||||
const cachedFilePath = path.join(cacheDir, filename);
|
||||
const cachedHashPath = `${cachedFilePath}.sha256`;
|
||||
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (version === "latest" || version === "nightly" || !fs.existsSync(cachedFilePath)) {
|
||||
console.info(`Downloading ${url} to ${cacheDir}`);
|
||||
// download file
|
||||
fs.writeFileSync(cachedFilePath, await got(url).buffer(), {
|
||||
flag: fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY,
|
||||
});
|
||||
console.info(`Downloaded ${url}`);
|
||||
|
||||
// ..and checksum
|
||||
console.info(`Downloading ${filename}.sha256`);
|
||||
fs.writeFileSync(cachedHashPath, await got(`${url}.sha256`).buffer(), {
|
||||
flag: fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY,
|
||||
});
|
||||
console.info(`Downloaded ${filename}.sha256`);
|
||||
} else {
|
||||
console.info(`Found ${cachedFilePath}`);
|
||||
}
|
||||
|
||||
console.info(`Verifying ${filename}.sha256`);
|
||||
|
||||
const digest = Buffer.alloc(64);
|
||||
const fd = fs.openSync(cachedHashPath, "r");
|
||||
fs.readSync(fd, digest, 0, digest.length, 0);
|
||||
fs.closeSync(fd);
|
||||
const expectedSha = digest.toString("utf8");
|
||||
const calculatedSha = await hasha.fromFile(cachedFilePath, {
|
||||
encoding: "hex",
|
||||
algorithm: "sha256",
|
||||
});
|
||||
if (calculatedSha !== expectedSha) {
|
||||
console.log(`calculatedSha: ${calculatedSha.length}`);
|
||||
console.log(`expectedSha: ${expectedSha.length}`);
|
||||
throw new Error(
|
||||
`sha256 mismatch for file ${cachedFilePath}. Expected '${expectedSha}', but calculated '${calculatedSha}'.
|
||||
Maybe the file downloaded was incomplete? Try to delete the file to force a re-download: ${cachedFilePath}`
|
||||
);
|
||||
}
|
||||
console.log(`OK (${expectedSha})`);
|
||||
|
||||
const data = fs.createReadStream(cachedFilePath);
|
||||
|
||||
await unpack(url, parentCacheDir, data);
|
||||
console.info(`Unpacked into ${parentCacheDir}`);
|
||||
|
||||
// Rename file if needed
|
||||
let resultingFileName = "odo";
|
||||
switch (platform) {
|
||||
case "windows":
|
||||
resultingFileName = "odo.exe";
|
||||
fs.renameSync(path.join(parentCacheDir, `odo-${platform}-${arch}.exe`), path.join(parentCacheDir, resultingFileName));
|
||||
break;
|
||||
case "darwin":
|
||||
fs.renameSync(path.join(parentCacheDir, `odo-${platform}-${arch}`), path.join(parentCacheDir, resultingFileName));
|
||||
break;
|
||||
case "linux":
|
||||
if (fs.existsSync(path.join(parentCacheDir, `odo-${platform}-${arch}`))) {
|
||||
fs.renameSync(path.join(parentCacheDir, `odo-${platform}-${arch}`), path.join(parentCacheDir, resultingFileName));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const p = path.join(parentCacheDir, resultingFileName);
|
||||
console.log(`odo binary (${version}) available at '${p}'`);
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {string} installPath
|
||||
* @param {import('stream').Readable} stream
|
||||
*/
|
||||
function unpack(url, installPath, stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (url.endsWith(".zip")) {
|
||||
return stream.pipe(
|
||||
unzip
|
||||
.Extract({ path: installPath })
|
||||
.on("close", resolve)
|
||||
.on("error", reject)
|
||||
);
|
||||
}
|
||||
|
||||
return stream
|
||||
.pipe(gunzip())
|
||||
.pipe(
|
||||
tarFS.extract(installPath).on("finish", resolve).on("error", reject)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {string} options.version
|
||||
* @param {string} options.platform
|
||||
* @param {string} options.arch
|
||||
*/
|
||||
async function download({ version, platform, arch }) {
|
||||
let versionToDl = version;
|
||||
if (versionToDl !== "latest" && versionToDl !== "nightly" && !versionToDl.startsWith("v")) {
|
||||
versionToDl = `v${version}`;
|
||||
}
|
||||
let url = `${ODO_DIST_URL}/${versionToDl}`;
|
||||
if (versionToDl === "nightly") {
|
||||
url = ODO_DIST_URL_NIGHTLY;
|
||||
}
|
||||
url += `/odo-${platform}-${arch}.${platform === "windows" ? "exe.zip" : "tar.gz"}`;
|
||||
return await cachingFetchAndVerify(url, platform, arch, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [platform]
|
||||
* @param {string} [arch]
|
||||
*/
|
||||
function enforcePlatformAndArch(platform, arch) {
|
||||
if (!SUPPORTED_ARCHITECTURES_BY_PLATFORM.has(platform)) {
|
||||
throw new Error(
|
||||
`No binary available for platform: ${platform}. Supported platforms: ${Array.from(
|
||||
SUPPORTED_ARCHITECTURES_BY_PLATFORM.keys()
|
||||
).join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
const archs = SUPPORTED_ARCHITECTURES_BY_PLATFORM.get(platform);
|
||||
if (!archs?.includes(arch)) {
|
||||
throw new Error(
|
||||
`No binary available for platform/arch: ${platform}/${arch}. Supported architectures for ${platform}: ${archs.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [version]
|
||||
* @param {string} [platform]
|
||||
* @param {string} [arch]
|
||||
*/
|
||||
function buildArguments(version, platform, arch) {
|
||||
const conf = pkgConf.sync("odo", {
|
||||
cwd: process.cwd(),
|
||||
defaults: {
|
||||
version: ODO_VERSION,
|
||||
distUrl: ODO_DIST_URL,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
version:
|
||||
process.env.BACKSTAGE_ODO_PLUGIN__ODO_VERSION || version || conf.version,
|
||||
platform:
|
||||
process.env.BACKSTAGE_ODO_PLUGIN__TARGET_OS || platform || goenv.GOOS,
|
||||
arch: process.env.BACKSTAGE_ODO_PLUGIN__TARGET_ARCH || arch || goenv.GOARCH,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [version]
|
||||
* @param {string} [platform]
|
||||
* @param {string} [arch]
|
||||
*/
|
||||
module.exports = async (version, platform, arch) => {
|
||||
const args = buildArguments(version, platform, arch);
|
||||
enforcePlatformAndArch(args.platform, args.arch);
|
||||
|
||||
return await download(args);
|
||||
};
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
"use strict";
|
||||
|
||||
function getGoOs() {
|
||||
if (process.platform === "win32") {
|
||||
return "windows";
|
||||
}
|
||||
|
||||
return process.platform;
|
||||
}
|
||||
|
||||
function getGoArch() {
|
||||
switch (process.arch) {
|
||||
case "ia32":
|
||||
return "386";
|
||||
case "x64":
|
||||
return "amd64";
|
||||
default:
|
||||
return process.arch;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GOOS: getGoOs(),
|
||||
GOARCH: getGoArch(),
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
"use strict";
|
||||
|
||||
const conf = require("pkg-conf").sync("odo", {
|
||||
cwd: process.cwd(),
|
||||
defaults: {
|
||||
skipDownload: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (conf.skipDownload) {
|
||||
console.info("Skipping download of odo as requested in package.json");
|
||||
} else {
|
||||
const download = require("./download");
|
||||
download().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './odo';
|
||||
export * from './odo-init';
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import type { Config } from '@backstage/config';
|
||||
import {
|
||||
createTemplateAction,
|
||||
executeShellCommand,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import cachedir from 'cachedir';
|
||||
import fs from 'fs-extra';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export const createOdoInitAction = ({
|
||||
odoConfig,
|
||||
}: {
|
||||
odoConfig: Config | undefined;
|
||||
}) => {
|
||||
return createTemplateAction<{
|
||||
devfile: string;
|
||||
version: string;
|
||||
starter_project: string | undefined;
|
||||
name: string;
|
||||
}>({
|
||||
id: 'devfile:odo:component:init',
|
||||
schema: {
|
||||
input: {
|
||||
required: ['devfile', 'version', 'name'],
|
||||
type: 'object',
|
||||
properties: {
|
||||
devfile: {
|
||||
type: 'string',
|
||||
title: 'Devfile',
|
||||
description: 'The Devfile',
|
||||
},
|
||||
version: {
|
||||
type: 'string',
|
||||
title: 'Version',
|
||||
description: 'The Devfile Stack version',
|
||||
},
|
||||
starter_project: {
|
||||
type: 'string',
|
||||
title: 'Starter Project',
|
||||
description: 'The starter project',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
title: 'Component name',
|
||||
description: 'The new component name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
ctx.logger.info(`Workspace: "${ctx.workspacePath}"`);
|
||||
ctx.logger.info(
|
||||
`Init "${ctx.input.name}" from: devfile=${ctx.input.devfile}, version=${ctx.input.version}, starterProject=${ctx.input.starter_project}...`,
|
||||
);
|
||||
|
||||
const telemetryDisabled =
|
||||
odoConfig?.getOptionalBoolean('telemetry.disabled') ?? false;
|
||||
ctx.logger.info(`...telemetry disabled: ${telemetryDisabled}`);
|
||||
|
||||
// Create a temporary file to use as dedicated config for odo
|
||||
const tmpDir = await fs.mkdtemp(join(tmpdir(), 'odo-init-'));
|
||||
const odoConfigFilePath = join(tmpDir, 'config');
|
||||
ctx.logger.info(`...temp dir for odo config: ${tmpDir}`);
|
||||
|
||||
const envVars = {
|
||||
// Due to a limitation in Node's child_process, the command lookup will be performed using options.env.PATH if options.env is defined.
|
||||
// See https://nodejs.org/docs/latest-v18.x/api/child_process.html#child_process_child_process
|
||||
...process.env,
|
||||
GLOBALODOCONFIG: odoConfigFilePath,
|
||||
ODO_TRACKING_CONSENT: telemetryDisabled ? 'no' : 'yes',
|
||||
TELEMETRY_CALLER: 'backstage',
|
||||
};
|
||||
|
||||
const randomRegistryName = 'GeneratedRegistryName';
|
||||
const devfileRegistryUrl =
|
||||
odoConfig?.getOptionalString('devfileRegistry.url') ??
|
||||
'https://registry.devfile.io';
|
||||
|
||||
ctx.logger.info(`...devfile registry URL: ${devfileRegistryUrl}`);
|
||||
|
||||
await fs.createFile(odoConfigFilePath);
|
||||
|
||||
let odoBinaryPath = odoConfig?.getOptionalString('binaryPath');
|
||||
if (!odoBinaryPath) {
|
||||
// Resolve from the downloaded dir
|
||||
odoBinaryPath = join(
|
||||
cachedir('odo'),
|
||||
`odo${process.platform === 'win32' ? '.exe' : ''}`,
|
||||
);
|
||||
if (!fs.existsSync(odoBinaryPath)) {
|
||||
// Fallback to any odo command available in the PATH
|
||||
ctx.logger.info(
|
||||
`odo binary path not set in app-config.yaml and not found in auto-download path (${odoBinaryPath}) => falling back to "odo" in the system PATH`,
|
||||
);
|
||||
odoBinaryPath = 'odo';
|
||||
}
|
||||
}
|
||||
ctx.logger.info(`odo binary path: ${odoBinaryPath}`);
|
||||
|
||||
// Add registry
|
||||
await executeShellCommand({
|
||||
command: odoBinaryPath,
|
||||
args: [
|
||||
'preference',
|
||||
'add',
|
||||
'registry',
|
||||
randomRegistryName,
|
||||
devfileRegistryUrl,
|
||||
],
|
||||
logStream: ctx.logStream,
|
||||
options: {
|
||||
cwd: ctx.workspacePath,
|
||||
env: envVars,
|
||||
},
|
||||
});
|
||||
|
||||
// odo init
|
||||
const initArgs = [
|
||||
'init',
|
||||
'--name',
|
||||
ctx.input.name,
|
||||
'--devfile-registry',
|
||||
randomRegistryName,
|
||||
'--devfile',
|
||||
ctx.input.devfile,
|
||||
'--devfile-version',
|
||||
ctx.input.version,
|
||||
];
|
||||
if (ctx.input.starter_project) {
|
||||
initArgs.push('--starter', ctx.input.starter_project);
|
||||
}
|
||||
await executeShellCommand({
|
||||
command: odoBinaryPath,
|
||||
args: initArgs,
|
||||
logStream: ctx.logStream,
|
||||
options: {
|
||||
cwd: ctx.workspacePath,
|
||||
env: envVars,
|
||||
},
|
||||
});
|
||||
|
||||
fs.rm(tmpDir, { recursive: true, maxRetries: 2, force: true }, () => {});
|
||||
|
||||
ctx.logger.info(
|
||||
`...Finished creating "${ctx.input.name}" from: devfile=${ctx.input.devfile}, version=${ctx.input.version}, starterProject=${ctx.input.starter_project}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import type { Config } from '@backstage/config';
|
||||
import {
|
||||
createTemplateAction,
|
||||
executeShellCommand,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import cachedir from 'cachedir';
|
||||
import fs from 'fs-extra';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export const createOdoAction = ({
|
||||
odoConfig,
|
||||
}: {
|
||||
odoConfig: Config | undefined;
|
||||
}) => {
|
||||
return createTemplateAction<{
|
||||
workingDirectory: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
}>({
|
||||
id: 'devfile:odo:command',
|
||||
schema: {
|
||||
input: {
|
||||
required: ['command'],
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: {
|
||||
type: 'string',
|
||||
title: 'Command',
|
||||
description: 'The odo command to run from the scaffolder workspace',
|
||||
},
|
||||
args: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
title: 'Arguments',
|
||||
description: 'Arguments to pass to the command',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx: any) {
|
||||
let args = [ctx.input.command];
|
||||
if (ctx.input.args?.length) {
|
||||
args = [...args, ...ctx.input.args];
|
||||
}
|
||||
|
||||
ctx.logger.info(`Workspace: "${ctx.workspacePath}"`);
|
||||
ctx.logger.info(`Running ${args}...`);
|
||||
|
||||
const telemetryDisabled =
|
||||
odoConfig?.getOptionalBoolean('telemetry.disabled') ?? false;
|
||||
ctx.logger.info(`...telemetry disabled: ${telemetryDisabled}`);
|
||||
|
||||
// Create a temporary file to use as dedicated config for odo
|
||||
const tmpDir = await fs.mkdtemp(join(tmpdir(), 'odo-'));
|
||||
const odoConfigFilePath = join(tmpDir, 'config');
|
||||
|
||||
const envVars = {
|
||||
// Due to a limitation in Node's child_process, the command lookup will be performed using options.env.PATH if options.env is defined.
|
||||
// See https://nodejs.org/docs/latest-v18.x/api/child_process.html#child_process_child_process
|
||||
...process.env,
|
||||
GLOBALODOCONFIG: odoConfigFilePath,
|
||||
ODO_TRACKING_CONSENT: telemetryDisabled ? 'no' : 'yes',
|
||||
TELEMETRY_CALLER: 'backstage',
|
||||
};
|
||||
|
||||
let odoBinaryPath = odoConfig?.getOptionalString('binaryPath');
|
||||
if (!odoBinaryPath) {
|
||||
// Resolve from the downloaded dir
|
||||
odoBinaryPath = join(
|
||||
cachedir('odo'),
|
||||
`odo${process.platform === 'win32' ? '.exe' : ''}`,
|
||||
);
|
||||
if (!fs.existsSync(odoBinaryPath)) {
|
||||
// Fallback to any odo command available in the PATH
|
||||
ctx.logger.info(
|
||||
`odo binary path not set in app-config.yaml and not found in auto-download path (${odoBinaryPath}) => falling back to "odo" in the system PATH`,
|
||||
);
|
||||
odoBinaryPath = 'odo';
|
||||
}
|
||||
}
|
||||
ctx.logger.info(`odo binary path: ${odoBinaryPath}`);
|
||||
|
||||
await fs.createFile(odoConfigFilePath);
|
||||
await executeShellCommand({
|
||||
command: odoBinaryPath,
|
||||
args: args,
|
||||
logStream: ctx.logStream,
|
||||
options: {
|
||||
cwd: ctx.workspacePath,
|
||||
env: envVars,
|
||||
},
|
||||
});
|
||||
fs.rm(tmpDir, { recursive: true, maxRetries: 2, force: true }, () => {});
|
||||
|
||||
ctx.logger.info(`Finished executing odo ${ctx.input.command}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './actions';
|
||||
export { odoModule as default } from './module';
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import {
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
|
||||
import { createOdoAction, createOdoInitAction } from './actions';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* The Odo Module for the Scaffolder Backend
|
||||
*/
|
||||
export const odoModule = createBackendModule({
|
||||
pluginId: 'scaffolder',
|
||||
moduleId: 'github',
|
||||
register({ registerInit }) {
|
||||
registerInit({
|
||||
deps: {
|
||||
scaffolder: scaffolderActionsExtensionPoint,
|
||||
config: coreServices.rootConfig,
|
||||
},
|
||||
async init({ scaffolder, config }) {
|
||||
const odoConfig = config.getOptionalConfig('odo');
|
||||
|
||||
scaffolder.addActions(
|
||||
createOdoAction({ odoConfig }),
|
||||
createOdoInitAction({ odoConfig }),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
"include": [
|
||||
"packages/*/src",
|
||||
"plugins/*/src",
|
||||
"plugins/*/dev",
|
||||
"plugins/*/migrations"
|
||||
],
|
||||
"files": ["node_modules/@backstage/cli/asset-types/asset-types.d.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist-types",
|
||||
"rootDir": ".",
|
||||
"lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2022"],
|
||||
"target": "ES2022",
|
||||
"useUnknownInCatchVariables": false
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue