azukiapp/azk

View on GitHub
src/manifest/index.js

Summary

Maintainability
D
1 day
Test Coverage
import { path, fs, config, _, t, log, lazy_require, isBlank } from 'azk';
import { System } from 'azk/system';
import { Validate } from 'azk/manifest/validate';
import { ManifestError, ManifestRequiredError, SystemNotFoundError } from 'azk/utils/errors';
import Utils from 'azk/utils';
import { Meta, FakeCache } from 'azk/manifest/meta';

var file_name = config('manifest');
var check     = require('syntax-error');
var tsort     = require('gaia-tsort');

var lazy = lazy_require({
  parent         : ['parentpath', 'sync'],
  runInNewContext: ['vm'],
});

var ManifestDsl = {
  console: console,
  require: require,
  env: process.env,
  disable: null,

  // Mounts
  path(folder, options = {}) {
    return { type: 'path', value: folder, options: options };
  },

  persistent(name, options = {}) {
    return { type: 'persistent', value: name, options: options };
  },

  sync(name, options = {}) {
    return { type: 'sync', value: name, options: options };
  },

  // Systems
  system(name, properties) {
    this.addSystem(name, properties);
  },

  systems(allSystems) {
    this.extendsSystems(allSystems);
    _.each(allSystems, (properties, name) => {
      this.addSystem(name, properties);
    });
  },

  // Extra options
  addImage(name, image) {
    this.images[name] = image;
  },

  registerBin(name, ...args) {
    this.bins[name] = [...args];
  },

  setCacheDir(dir) {
    this.cache_dir = dir;
  },

  setDefault(name) {
    this.default = name;
  },
};

export class Manifest {
  constructor(cwd, file = null, required = false) {
    if (typeof file == "boolean") {
      [required, file] = [file, null];
    }

    if (required && !cwd) {
      throw new Error(t("manifest.required_path"));
    }

    this.cwd_search = cwd;
    this.images     = {};
    this.systems    = {};
    this.bins       = {};
    this._default   = null;
    this.file       = file || Manifest.find_manifest(cwd);

    if (required && !this._exist()) {
      throw new ManifestRequiredError(cwd);
    }

    // Create cache for application status
    if (_.isEmpty(this.cache_dir) && this._exist()) {
      this.cache_dir = path.join(this.cwd, config('azk_dir'), this._file_relative());
    }
    this.meta = new Meta(this);

    if (this._exist()) {
      this.parse();
    }
  }

  // Validate
  validate(...args) {
    return Validate.analyze(this, ...args);
  }

  parse() {
    var content  = fs.readFileSync(this.file);
    let err_file = path.relative(this.cwd_search, this.file);
    var err  = check(content.toString(), this.file);
    if (err) {
      throw new ManifestError(err_file, err, 'syntax');
    } else {
      try {
        lazy.runInNewContext(content, Manifest.createDslContext(this), this.file);
      } catch (e) {
        if (!(e instanceof ManifestError)) {
          var stack = e.stack.split('\n');
          var msg   = stack[0] + "\n" + stack[1];
          log.info('Manifest parse error %s', e.stack);
          throw new ManifestError(err_file, msg, 'logic');
        }
        throw e;
      }
    }
    this.systemsInOrder();
  }

  static createDslContext(target) {
    return _.reduce(ManifestDsl, (context, func, name) => {
      if (_.isFunction(func)) {
        context[name] = func.bind(target);
      } else {
        context[name] = func;
      }
      return context;
    }, { });
  }

  extendsSystems(allSystems) {
    _.each(allSystems, (properties, name) => {
      if (!(properties instanceof System)) {
        if (properties.extends) {
          let raw = _.cloneDeep(properties);

          // validate is extends system exists
          if (!allSystems[properties.extends]) {
            var msg = t("manifest.extends_system_invalid", { system_source: properties.extends,
              system_to_extend: name });
            throw new ManifestError(this.file, msg);
          }

          var sourceSystem = _.cloneDeep(allSystems[properties.extends]);
          var destinationSystem = allSystems[name];

          // if "depends" or "image" is null ignore these properties
          if (isBlank(destinationSystem.depends)) {
            delete destinationSystem.depends;
          }
          if (isBlank(destinationSystem.image)) {
            delete destinationSystem.image;
          }

          // get all from sourceSystem but override with destinationSystem
          _.assign(sourceSystem, destinationSystem);
          allSystems[name] = sourceSystem;

          // Set raw data
          allSystems[name].raw = raw;
        }
      }
    });

    return allSystems;
  }

  addSystem(name, properties) {
    if (!(properties instanceof System)) {
      properties.raw = properties.raw || _.cloneDeep(properties);
      this._system_validate(name, properties);
      var image = properties.image;
      delete properties.image;
      properties = new System(this, name, image, properties);
    }

    this.systems[name] = properties;
    if (!this._default) {
      this._default = name;
    }

    return this;
  }

  // TODO: refactoring to use validate
  _system_validate(name, properties) {
    var msg, opts;
    // system_name must not contain anything not valid in docker container name
    if (!name.match(/^[a-zA-Z0-9-]+$/)) {
      msg = t("manifest.system_name_invalid", { system: name });
      throw new ManifestError(this.file, msg);
    }
    if (properties.extends === name) {
      msg = t("manifest.cannot_extends_itself", { system: name });
      throw new ManifestError(this.file, msg);
    }
    if (_.isEmpty(properties.image)) {
      msg = t("manifest.image_required", { system: name });
      throw new ManifestError(this.file, msg);
    }
    if (!_.isEmpty(properties.balancer)) {
      msg = t("manifest.balancer_deprecated", { system: name });
      throw new ManifestError(this.file, msg);
    }
    if (!_.isEmpty(properties.mount_folders)) {
      opts = { option: 'mount_folders', system: name, manifest: this.file };
      msg  = t("manifest.mount_and_persistent_deprecated", opts);
      throw new ManifestError(this.file, msg);
    }
    if (!_.isEmpty(properties.persistent_folders)) {
      opts = { option: 'persistent_folders', system: name, manifest: this.file };
      msg  = t("manifest.mount_and_persistent_deprecated", opts);
      throw new ManifestError(this.file, msg);
    }

    // Not support docker_extra.start and docker_extra.create
    var extra = properties.docker_extra;
    if (_.has(extra, "start") || _.has(properties, "create")) {
      var option = _.has(extra, "start") ? "start" : "create";
      opts = { option: `docker_extra.${option}`, system: name, manifest: this.file };
      msg  = t("manifest.extra_docker_start_deprecated", opts);
      throw new ManifestError(this.file, msg);
    }
  }

  system(name, isRequired = false) {
    var sys = this.systems[name];

    if (isRequired && !sys) {
      throw new SystemNotFoundError(this.file, name);
    }

    return sys;
  }

  getSystemsByName(names) {
    var systems_name = this.systemsInOrder();

    if (!_.isEmpty(names)) {
      names = _.isArray(names) ? names : names.split(',');
      _.each(names, (name) => this.system(name, true));
      systems_name = _.intersection(systems_name, names);
    }

    return _.reduce(systems_name, (systems, name) => {
      systems.push(this.system(name, true));
      return systems;
    }, []);
  }

  systemsInOrder(requireds = []) {
    var edges = [];
    _.each(this.systems, (system, name) => {
      if (_.isEmpty(system.depends)) {
        edges.push(["__", name]);
      } else {
        _.each(system.depends, (depend) => {
          if (this.system(depend)) {
            edges.push([depend, name]);
          } else {
            var msg = t("manifest.depends_not_declared", {
              system: name,
              depend: depend,
            });
            throw new ManifestError(this.file, msg);
          }
        });
      }
    });

    var result = tsort(edges);
    if (result.error) {
      var properties = result.error.message.match(/^(.*?)\s.*\s(.*)$/);
      var msg  = t("manifest.circular_dependency", {
        system1: properties[1], system2: properties[2]
      });
      throw new ManifestError(this.file, msg);
    }

    var path = _.isEmpty(requireds) ? result.path : [];
    if (_.isEmpty(path)) {
      requireds = _.isArray(requireds) ? requireds : [requireds];
      path = _.reduce(requireds, (path, node) => {
        return this.__putNodesInPath(result.graph, path, node);
      }, path);
    }

    return path.slice(1);
  }

  __putNodesInPath(graph, path, node_id) {
    var node = _.find(graph, (node) => { return node.id == node_id; });
    if (!_.isEmpty(node)) {
      path = _.reduce(node.parents, (path, parent) => {
        return this.__putNodesInPath(graph, path, parent);
      }, path);
      if (!_.contains(path, node_id)) {
        path.push(node_id);
      }
    } else {
      throw new SystemNotFoundError(this.file, node_id);
    }
    return path;
  }

  // Meta forwarding
  getMeta(...args) {
    return this.meta.getOrSet(...args);
  }

  setMeta(...args) {
    this.meta.set(...args);
    return this;
  }

  cleanMetaAsync(...args) {
    return this.meta.cleanAsync(...args);
  }

  // Default system
  set default(nameOrSystem) {
    if (nameOrSystem instanceof System) {
      nameOrSystem = nameOrSystem.name;
    }

    if (!(this.systems[nameOrSystem] instanceof System)) {
      var msg = t('manifest.invalid_default', { system: nameOrSystem });
      throw new ManifestError(this.file, msg);
    }

    this._default = nameOrSystem;
  }

  get default() {
    return this._default;
  }

  get systemDefault() {
    return this.system(this._default);
  }

  // Getters
  get manifestPath() {
    return this.cwd;
  }

  get manifestDirName() {
    return path.basename(this.manifestPath);
  }

  get file() {
    return this.__file;
  }

  set file(value) {
    this.cwd = path.dirname(value);
    this.__file = value;
  }

  _exist() {
    return fs.existsSync(this.file);
  }

  _file_relative() {
    return path.relative(this.manifestPath, this.file);
  }

  // TODO: make faster
  get namespace() {
    var def = Utils.calculateHash(this.file).slice(0, 10);
    return this.meta.getOrSet('namespace', def);
  }

  set cache_dir(value) {
    this.__cache_dir = value;
  }

  get cache_dir() {
    return this.__cache_dir;
  }

  static find_manifest(target) {
    var dir = Utils.cd(target, function() {
      return lazy.parent(file_name);
    });
    return dir ? path.join(dir, file_name) : null;
  }

  static makeFake(cwd, image) {
    var file = path.join(cwd, file_name);
    var manifest = new FakeManifest(null, file);

    return manifest.addSystem("--tmp--", {
      image: image,
      workdir: "/azk/#{manifest.dir}",
      mounts: {
        "/azk/#{manifest.dir}": "#{manifest.path}"
      }
    });
  }
}

class FakeManifest extends Manifest {
  constructor(...args) {
    super(...args);
    this.meta.cache = new FakeCache();
  }
  parse() {}
}

export { file_name, System };