Back to Backstage

Template Testing Dry Run

contrib/scaffolder/template-testing-dry-run.md

1.51.0-next.24.3 KB
Original Source
js
/*
 * Copyright 2022 The Backstage Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * A CLI that helps you test Backstage software templates
 *
 * @packageDocumentation
 */

import { dirname, join, relative } from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
import { gzipSync } from 'node:zlib';
import { ensureDir } from 'fs-extra';
import { program } from 'commander';
import ora from 'ora';
import readdir from 'recursive-readdir';
import { parse } from 'yaml';
import { version } from '../../../package.json';
import type { ScaffolderDryRunResponse } from '@backstage/plugin-scaffolder';

const loadDirectoryContents = async (template_path: string) => {
  const spinner = ora('Loading template').start();
  const files = await await readdir(template_path, ['.git']);
  const contents = await Promise.all(
    files.map(async p => {
      return {
        path: relative(template_path, p),
        base64Content: (await readFile(p)).toString('base64'),
      };
    }),
  );
  spinner.succeed();
  return contents;
};

const writeResultContents = async (
  root: string,
  directoryContents: ScaffolderDryRunResponse['directoryContents'],
) => {
  const directories = new Set(
    directoryContents.map(({ path }) => dirname(join(root, path))),
  );
  await Promise.all(Array.from(directories).map(d => ensureDir(d)));
  await Promise.all(
    directoryContents.map(async ({ path, base64Content, executable }) =>
      writeFile(join(root, path), Buffer.from(base64Content, 'base64'), {
        mode: executable ? 0o755 : 0o644,
      }),
    ),
  );
};

const api = async (
  bodyObj: Record<string, any>,
  { baseURL, token }: { baseURL: string; token: string | false },
) => {
  const body = gzipSync(JSON.stringify(bodyObj));
  return fetch(new URL('/api/scaffolder/v2/dry-run', baseURL), {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token || ''}`,
      'Content-Type': 'application/json',
      'Content-Encoding': 'gzip',
    },
    body,
  });
};

const handle = async (
  baseURL: string,
  template_path: string,
  data: string,
  target: string,
  { token }: { token: string | false },
) => {
  const directoryContents = await loadDirectoryContents(template_path);
  const values = parse(await readFile(data, 'utf-8'));
  const secrets = {};
  const template = parse(
    await readFile(`${template_path}/template.yaml`, 'utf-8'),
  );
  const response = await api(
    {
      directoryContents,
      values,
      secrets,
      template,
    },
    { baseURL, token },
  );
  if (!response.ok) {
    const contentType = response.headers.get('content-type');
    if (contentType?.startsWith('application/json')) {
      const responseData = await response.json();
      if (responseData.error) {
        throw responseData.error;
      }
      if (responseData.errors) {
        throw responseData.errors;
      }
      throw responseData;
    }
    throw await response.text();
  }
  const { log, directoryContents: resultContents } =
    (await response.json()) as ScaffolderDryRunResponse;
  for (const logEntry of log) {
    console.log(logEntry.body.message);
  }
  await writeResultContents(target, resultContents);
};

const main = async (argv: string[]) => {
  program
    .name('backstage-scaffolder')
    .version(version)
    .description('Creates a dry-run output of a given template')
    .option('-t, --token <value>', 'JWT to use for auth')
    .argument('url', 'URL of your Backstage instance')
    .argument('template-path', 'Source directory of template')
    .argument('data-path', 'YAML file with input to render')
    .argument('target', 'Output directory')
    .action(handle);

  await program.parseAsync(argv);
  process.exit();
};

process.on('unhandledRejection', rejection => {
  console.error(rejection);
  process.exit(1);
});

main(process.argv);