serverless-python-requirements
Version: 
Serverless Python Requirements Plugin
206 lines (188 loc) • 5.76 kB
JavaScript
const spawn = require('child-process-ext/spawn');
const isWsl = require('is-wsl');
const fse = require('fs-extra');
const path = require('path');
const os = require('os');
/**
 * Helper function to run a docker command
 * @param {string[]} options
 * @return {Object}
 */
async function dockerCommand(options, pluginInstance) {
  const cmd = 'docker';
  try {
    return await spawn(cmd, options, { encoding: 'utf-8' });
  } catch (e) {
    if (
      e.stderrBuffer &&
      e.stderrBuffer.toString().includes('command not found')
    ) {
      throw new pluginInstance.serverless.classes.Error(
        'docker not found! Please install it.',
        'PYTHON_REQUIREMENTS_DOCKER_NOT_FOUND'
      );
    }
    throw e;
  }
}
/**
 * Build the custom Docker image
 * @param {string} dockerFile
 * @param {string[]} extraArgs
 * @return {string} The name of the built docker image.
 */
async function buildImage(dockerFile, extraArgs, pluginInstance) {
  const imageName = 'sls-py-reqs-custom';
  const options = ['build', '-f', dockerFile, '-t', imageName];
  if (Array.isArray(extraArgs)) {
    options.push(...extraArgs);
  } else {
    throw new pluginInstance.serverless.classes.Error(
      'dockerRunCmdExtraArgs option must be an array',
      'PYTHON_REQUIREMENTS_INVALID_DOCKER_EXTRA_ARGS'
    );
  }
  options.push('.');
  await dockerCommand(options, pluginInstance);
  return imageName;
}
/**
 * Find a file that exists on all projects so we can test if Docker can see it too
 * @param {string} servicePath
 * @return {string} file name
 */
function findTestFile(servicePath, pluginInstance) {
  if (fse.pathExistsSync(path.join(servicePath, 'serverless.yml'))) {
    return 'serverless.yml';
  }
  if (fse.pathExistsSync(path.join(servicePath, 'serverless.yaml'))) {
    return 'serverless.yaml';
  }
  if (fse.pathExistsSync(path.join(servicePath, 'serverless.json'))) {
    return 'serverless.json';
  }
  if (fse.pathExistsSync(path.join(servicePath, 'requirements.txt'))) {
    return 'requirements.txt';
  }
  throw new pluginInstance.serverless.classes.Error(
    'Unable to find serverless.{yml|yaml|json} or requirements.txt for getBindPath()',
    'PYTHON_REQUIREMENTS_MISSING_GET_BIND_PATH_FILE'
  );
}
/**
 * Test bind path to make sure it's working
 * @param {string} bindPath
 * @return {boolean}
 */
async function tryBindPath(bindPath, testFile, pluginInstance) {
  const { serverless, log } = pluginInstance;
  const debug = process.env.SLS_DEBUG;
  const options = [
    'run',
    '--rm',
    '-v',
    `${bindPath}:/test`,
    'alpine',
    'ls',
    `/test/${testFile}`,
  ];
  try {
    if (debug) {
      if (log) {
        log.debug(`Trying bindPath ${bindPath} (${options})`);
      } else {
        serverless.cli.log(`Trying bindPath ${bindPath} (${options})`);
      }
    }
    const ps = await dockerCommand(options, pluginInstance);
    if (debug) {
      if (log) {
        log.debug(ps.stdoutBuffer.toString().trim());
      } else {
        serverless.cli.log(ps.stdoutBuffer.toString().trim());
      }
    }
    return ps.stdoutBuffer.toString().trim() === `/test/${testFile}`;
  } catch (err) {
    if (debug) {
      if (log) {
        log.debug(`Finding bindPath failed with ${err}`);
      } else {
        serverless.cli.log(`Finding bindPath failed with ${err}`);
      }
    }
    return false;
  }
}
/**
 * Get bind path depending on os platform
 * @param {object} serverless
 * @param {string} servicePath
 * @return {string} The bind path.
 */
async function getBindPath(servicePath, pluginInstance) {
  // Determine bind path
  let isWsl1 = isWsl && !os.release().includes('microsoft-standard');
  if (process.platform !== 'win32' && !isWsl1) {
    return servicePath;
  }
  // test docker is available
  await dockerCommand(['version'], pluginInstance);
  // find good bind path for Windows
  let bindPaths = [];
  let baseBindPath = servicePath.replace(/\\([^\s])/g, '/$1');
  let drive;
  let path;
  bindPaths.push(baseBindPath);
  if (baseBindPath.startsWith('/mnt/')) {
    // cygwin "/mnt/C/users/..."
    baseBindPath = baseBindPath.replace(/^\/mnt\//, '/');
  }
  if (baseBindPath[1] == ':') {
    // normal windows "c:/users/..."
    drive = baseBindPath[0];
    path = baseBindPath.substring(3);
  } else if (baseBindPath[0] == '/' && baseBindPath[2] == '/') {
    // gitbash "/c/users/..."
    drive = baseBindPath[1];
    path = baseBindPath.substring(3);
  } else {
    throw new Error(`Unknown path format ${baseBindPath.substr(10)}...`);
  }
  bindPaths.push(`/${drive.toLowerCase()}/${path}`); // Docker Toolbox (seems like Docker for Windows can support this too)
  bindPaths.push(`${drive.toLowerCase()}:/${path}`); // Docker for Windows
  // other options just in case
  bindPaths.push(`/${drive.toUpperCase()}/${path}`);
  bindPaths.push(`/mnt/${drive.toLowerCase()}/${path}`);
  bindPaths.push(`/mnt/${drive.toUpperCase()}/${path}`);
  bindPaths.push(`${drive.toUpperCase()}:/${path}`);
  const testFile = findTestFile(servicePath, pluginInstance);
  for (let i = 0; i < bindPaths.length; i++) {
    const bindPath = bindPaths[i];
    if (await tryBindPath(bindPath, testFile, pluginInstance)) {
      return bindPath;
    }
  }
  throw new Error('Unable to find good bind path format');
}
/**
 * Find out what uid the docker machine is using
 * @param {string} bindPath
 * @return {boolean}
 */
async function getDockerUid(bindPath, pluginInstance) {
  const options = [
    'run',
    '--rm',
    '-v',
    `${bindPath}:/test`,
    'alpine',
    'stat',
    '-c',
    '%u',
    '/bin/sh',
  ];
  const ps = await dockerCommand(options, pluginInstance);
  return ps.stdoutBuffer.toString().trim();
}
module.exports = { buildImage, getBindPath, getDockerUid };