zodern/meteor-up

View on GitHub
src/plugins/meteor/prepare-bundle.js

Summary

Maintainability
A
3 hrs
Test Coverage
import { checkCompatible, renameTag } from './docker-registry';
import {
  escapeEnvQuotes,
  getImagePrefix,
  getNodeVersion,
  runCommand
} from './utils';
import fs from 'fs';

export function prepareBundleSupported(dockerConfig) {
  const supportedImages = ['abernix/meteord', 'zodern/meteor'];

  if ('prepareBundle' in dockerConfig) {
    return dockerConfig.prepareBundle;
  }

  return supportedImages.find(
    supportedImage => dockerConfig.image.indexOf(supportedImage) === 0
  ) || false;
}

export function createDockerFile(appConfig) {
  const {
    env,
    docker: {
      useBuildKit,
      image
    } = {}
  } = appConfig;
  const escapedEnv = escapeEnvQuotes(env);

  const syntax = useBuildKit ? '# syntax=docker/dockerfile:1-experimental' : '';
  const args = Object.entries(escapedEnv).map(([key, value]) => `ARG ${key}="${value}"`).join('\n');
  const copy = useBuildKit ?
    'RUN --mount=type=bind,target=/tmp/__mup-bundle tar -xzf /tmp/__mup-bundle/bundle.tar.gz -C /built_app --strip-components=1' :
    'COPY ./ /built_app';

  return `
    ${syntax}
    FROM ${image}
    RUN mkdir /built_app || true
    ${args}
    # TODO: build instructions
    ${copy}
    RUN cd /built_app/programs/server && \
      npm install --unsafe-perm
  `.trim();
}

export async function prepareBundleLocally(
  buildLocation, bundlePath, api
) {
  const {
    app: appConfig,
    privateDockerRegistry
  } = api.getConfig();
  const startTime = new Date();

  if (!appConfig.docker.useBuildKit) {
    const error = new Error('useBuildKit must be enabled when using prepareBundleLocally');
    error.solution = 'Set app.docker.useBuildKit to true in your config.';
    throw error;
  }

  const useRegistryApi = await checkCompatible(privateDockerRegistry);
  const nodeVersion = await getNodeVersion(bundlePath);
  const image = `${getImagePrefix(privateDockerRegistry)}${appConfig.name}`;
  const dockerFile = createDockerFile(appConfig);
  const dockerIgnoreContent = `
*
!bundle.tar.gz
`;

  console.log('=> Writing Dockerfile');
  fs.writeFileSync(api.resolvePath(buildLocation, 'Dockerfile'), dockerFile);
  fs.writeFileSync(api.resolvePath(buildLocation, '.dockerignore'), dockerIgnoreContent);

  console.log('');
  console.log('=> Updating base image');
  await runCommand(
    'docker',
    [
      'login',
      '--username',
      privateDockerRegistry.username,
      '--password',
      privateDockerRegistry.password
    ].concat(privateDockerRegistry.host ? [privateDockerRegistry.host] : [])
  );
  await runCommand('docker', ['pull', appConfig.docker.image]);

  console.log('');
  console.log('=> Build image');

  if (appConfig.docker.useBuildKit) {
    process.env.DOCKER_BUILDKIT = '1';
  }

  await runCommand('docker', [
    'build',
    '-t', `${image}:build`,
    '.',
    '--build-arg', `NODE_VERSION=${nodeVersion}`
  ], { cwd: buildLocation });

  console.log('');
  console.log('=> Updating tags');

  // Rename previous tag to latest
  // If we can't use the registry api, we pull the latest image, tag it
  // locally, and then push it
  if (useRegistryApi) {
    await renameTag({
      registryConfig: privateDockerRegistry,
      image,
      oldTag: 'latest',
      newTag: 'previous',
      ignoreIfMissing: true
    });
    await runCommand('docker', [
      'tag',
      `${image}:build`,
      `${image}:latest`
    ]);
    await runCommand('docker', [
      'push',
      `${image}:latest`
    ]);
  } else {
    console.log('Not able to use the api provided by this docker registry. Falling back to the slower method to rename tags.');
    await runCommand('docker', ['pull', `${image}:latest`]);
    await runCommand('docker', [
      'tag',
      `${image}:latest`,
      `${image}:previous`
    ]);
    await runCommand('docker', [
      'tag',
      `${image}:build`,
      `${image}:latest`
    ]);
    await runCommand('docker', [
      'push',
      `${image}:previous`
    ]);
    await runCommand('docker', [
      'push',
      `${image}:latest`
    ]);
  }

  const endTime = new Date();
  const durationText = `in ${endTime.getTime() - startTime.getTime()}ms`;

  console.log('=> Finished preparing bundle', durationText);
}