Skip to content

Commit

Permalink
fix(cli): improve local development experience (janus-idp#2299)
Browse files Browse the repository at this point in the history
* fix(cli): improve example configuration display

This change changes the CLI package-dynamic-plugins command to take
advantage of some dynamic plugin projects that have followed the
convention of maintaining a app-config.janus-idp.yaml in the root of the
plugin source directory.  When this file is present, it will be read in
as the command processes dynamic plugins in the monorepo and then
included in the example dynamic-plugins.yaml configuration printed out
when the command successfully completes.  This change also adjusts the
control flow where this is printed out to ensure it's after successful
completion of the container image.

Signed-off-by: Stan Lewis <gashcrumb@gmail.com>

* feat(cli): add flag to specify output directory

This change adds an --export-to flag to the package-dynamic-plugins
CLI command. This flag will skip the container build step, and instead
copy the staged dynamic plugin assets to the specified output directory.
The addition of this flag makes the --tag argument optional, and only
required if --export-to is not specified.

Signed-off-by: Stan Lewis <gashcrumb@gmail.com>

---------

Signed-off-by: Stan Lewis <gashcrumb@gmail.com>
  • Loading branch information
gashcrumb authored and 04kash committed Nov 4, 2024
1 parent 8d2ec95 commit cbbb886
Show file tree
Hide file tree
Showing 6 changed files with 1,477 additions and 1,293 deletions.
15 changes: 15 additions & 0 deletions .changeset/eighty-tips-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@janus-idp/cli": patch
---

fix(cli): Improve example configuration display

This change changes the CLI package-dynamic-plugins command to take
advantage of some dynamic plugin projects that have followed the
convention of maintaining a app-config.janus-idp.yaml in the root of the
plugin source directory. When this file is present, it will be read in
as the command processes dynamic plugins in the monorepo and then
included in the example dynamic-plugins.yaml configuration printed out
when the command successfully completes. This change also adjusts the
control flow where this is printed out to ensure it's after successful
completion of the container image.
7 changes: 7 additions & 0 deletions .changeset/mean-eagles-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@janus-idp/cli": minor
---

feat(cli) add flag to specify output directory

This change adds an --export-to flag to the package-dynamic-plugins CLI command. This flag will skip the container build step, and instead copy the staged dynamic plugin assets to the specified output directory. The addition of this flag makes the --tag argument optional, and only required if --export-to is not specified.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"typescript-json-schema": "^0.64.0",
"webpack": "^5.89.0",
"webpack-dev-server": "^4.15.1",
"yaml": "^2.5.1",
"yml-loader": "^2.1.0",
"yn": "^4.0.0"
},
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,13 @@ export function registerScriptCommand(program: Command) {
'--preserve-temp-dir',
'Leave the temporary staging directory on the filesystem instead of deleting it',
)
.requiredOption(
.option(
'--export-to <directory>',
'Export the plugins to the specified directory, skips building the container image',
)
.option(
'-t, --tag <tag>',
'Tag name to use when building the plugin registry image',
'Tag name to use when building the plugin registry image. Required if "--export-to" is not specified',
)
.option(
'--use-docker',
Expand Down
147 changes: 116 additions & 31 deletions packages/cli/src/commands/package-dynamic-plugins/command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { PackageRoles } from '@backstage/cli-node';

import chalk from 'chalk';
import { OptionValues } from 'commander';
import fs from 'fs-extra';
import YAML from 'yaml';

import os from 'os';
import path from 'path';
Expand All @@ -10,18 +12,25 @@ import { paths } from '../../lib/paths';
import { Task } from '../../lib/tasks';

export async function command(opts: OptionValues): Promise<void> {
const { forceExport, preserveTempDir, tag, useDocker } = opts;
const containerTool = useDocker ? 'docker' : 'podman';
// check if the container tool is available
try {
await Task.forCommand(`${containerTool} --version`);
} catch (e) {
const { exportTo, forceExport, preserveTempDir, tag, useDocker } = opts;
if (!exportTo && !tag) {
Task.error(
`Unable to find ${containerTool} command: ${e}\nMake sure that ${containerTool} is installed and available in your PATH.`,
`Neither ${chalk.white('--export-to')} or ${chalk.white('--tag')} was specified, either specify ${chalk.white('--export-to')} to export plugins to a directory or ${chalk.white('--tag')} to export plugins to a container image`,
);
return;
}

const containerTool = useDocker ? 'docker' : 'podman';
// check if the container tool is available, skip if just exporting the plugins to a directory
if (!exportTo) {
try {
await Task.forCommand(`${containerTool} --version`);
} catch (e) {
Task.error(
`Unable to find ${containerTool} command: ${e}\nMake sure that ${containerTool} is installed and available in your PATH.`,
);
return;
}
}
const workspacePackage = await fs.readJson(
paths.resolveTarget('package.json'),
);
Expand Down Expand Up @@ -73,6 +82,7 @@ export async function command(opts: OptionValues): Promise<void> {
path.join(os.tmpdir(), 'package-dynamic-plugins'),
);
const pluginRegistryMetadata = [];
const pluginConfigs: Record<string, string> = {};
try {
// copy the dist-dynamic output folder for each plugin to some temp directory and generate the metadata entry for each plugin
for (const pluginPkg of packages) {
Expand All @@ -92,7 +102,12 @@ export async function command(opts: OptionValues): Promise<void> {
const targetDirectory = path.join(tmpDir, packageName);
Task.log(`Copying '${distDirectory}' to '${targetDirectory}`);
try {
fs.cpSync(distDirectory, targetDirectory, { recursive: true });
// Copy the exported package to the staging area and ensure symlinks
// are copied as normal folders
fs.cpSync(distDirectory, targetDirectory, {
recursive: true,
dereference: true,
});
const {
name,
version,
Expand Down Expand Up @@ -121,6 +136,25 @@ export async function command(opts: OptionValues): Promise<void> {
keywords,
},
});
// some plugins include configuration snippets in an app-config.janus-idp.yaml
const pluginConfigPath =
discoverPluginConfigurationFile(packageDirectory);
if (typeof pluginConfigPath !== 'undefined') {
try {
const pluginConfig = fs.readFileSync(pluginConfigPath);
pluginConfigs[packageName] = YAML.parse(
pluginConfig.toLocaleString(),
);
} catch (err) {
Task.log(
`Encountered an error parsing configuration at ${pluginConfigPath}, no example configuration will be displayed`,
);
}
} else {
Task.log(
`No plugin configuration found at ${pluginConfigPath} create this file as needed if this plugin requires configuration`,
);
}
} catch (err) {
Task.log(
`Encountered an error copying static assets for plugin ${packageFilePath}, the plugin will not be packaged. The error was ${err}`,
Expand All @@ -134,15 +168,54 @@ export async function command(opts: OptionValues): Promise<void> {
metadataFile,
JSON.stringify(pluginRegistryMetadata, undefined, 2),
);
// run the command to generate the image
Task.log(`Creating image using ${containerTool}`);
await Task.forCommand(
`echo "from scratch
if (exportTo) {
// copy the temporary directory contents to the target directory
fs.mkdirSync(exportTo, { recursive: true });
Task.log(`Writing exported plugins to ${exportTo}`);
fs.readdirSync(tmpDir).forEach(entry => {
const source = path.join(tmpDir, entry);
const destination = path.join(exportTo, entry);
fs.copySync(source, destination, { recursive: true, overwrite: true });
});
} else {
// run the command to generate the image
Task.log(`Creating image using ${containerTool}`);
await Task.forCommand(
`echo "from scratch
COPY . .
" | ${containerTool} build --annotation com.redhat.rhdh.plugins='${JSON.stringify(pluginRegistryMetadata)}' -t '${tag}' -f - .
`,
{ cwd: tmpDir },
);
{ cwd: tmpDir },
);
Task.log(`Successfully built image ${tag} with following plugins:`);
for (const plugin of pluginRegistryMetadata) {
Task.log(` ${chalk.white(Object.keys(plugin)[0])}`);
}
}
// print out a configuration example based on available plugin data
try {
const configurationExample = YAML.stringify({
plugins: pluginRegistryMetadata.map(plugin => {
const packageName = Object.keys(plugin)[0];
const pluginConfig = pluginConfigs[packageName];
const packageString = exportTo
? `./local-plugins/${packageName}`
: `oci://${tag}!${packageName}`;
return {
package: packageString,
disabled: false,
...(pluginConfig ? { pluginConfig } : {}),
};
}),
});
Task.log(
`\nHere is an example dynamic-plugins.yaml for these plugins: \n\n${chalk.white(configurationExample)}\n\n`,
);
} catch (err) {
Task.error(
`An error occurred while creating configuration example: ${err}`,
);
}
} catch (e) {
Task.error(`Error encountered while packaging dynamic plugins: ${e}`);
} finally {
Expand All @@ -153,22 +226,6 @@ COPY . .
if (preserveTempDir) {
Task.log(`Keeping temporary directory ${tmpDir}`);
}

Task.log(`Successfully built image ${tag} with following plugins:`);
for (const plugin of pluginRegistryMetadata) {
Task.log(` ${Object.keys(plugin)[0]}`);
}
Task.log(`
Configuration example for the dynamic-plugins.yaml:
packages:`);
for (const plugin of pluginRegistryMetadata) {
Task.log(`- package: oci://${tag}!${Object.keys(plugin)[0]}
disabled: false
pluginConfig:
# add required plugin configuration here
`);
}
} catch (err) {
Task.error(
`An error occurred while removing the temporary staging directory: ${err}`,
Expand Down Expand Up @@ -225,6 +282,34 @@ async function discoverPluginPackages() {
});
}

/**
* Scans the specified directory for plugin configuration files that
* match known potential file names.
* @param directory
* @returns
*/
function discoverPluginConfigurationFile(
directory: string,
): string | undefined {
// Possible file names, the first match will be used
const supportedFilenames = [
'app-config.janus-idp.yaml',
'app-config.backstage-community.yaml',
'app-config.yaml',
];
return supportedFilenames
.map<boolean>((fileName: string) => {
const candidate = path.join(directory, fileName);
return fs.existsSync(candidate);
})
.reduce<string | undefined>((val, current, index) => {
if (typeof val === 'undefined' && current) {
return path.join(directory, supportedFilenames[index]);
}
return val;
}, undefined);
}

/**
* Scan all subdirectories for the included files, skipping any excluded
* directories.
Expand Down
Loading

0 comments on commit cbbb886

Please sign in to comment.