ContainerHQ/arkis-api

View on GitHub
app/models/cluster.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

let _ = require('lodash'),
  concerns = require('./concerns'),
  config = require('../../config'),
  Cert = require('../connectors').Cert,
  is = require('./validators');

const CONCERNS = {
  serializable: {
    omit:  ['cert', 'encrypted_cert', 'last_state', 'user_id'],
    links: ['nodes']
  },
  state: {
    attribute: {
      name: 'last_state',
      default: 'empty',
      values: ['empty', 'deploying', 'upgrading', 'updating', 'running']
    },
    expiration: {
      when: 'running',
      mustBe: 'unreachable',
      constraint: {
        name: 'last_seen'
      },
      after: config.agent.heartbeat,
    }
  },
  encrypted: ['cert']
};

module.exports = function(sequelize, DataTypes) {
  let { attributes, options } = concerns.extend({
    attributes: {
      id: {
        type: DataTypes.UUID,
        primaryKey: true,
        defaultValue: DataTypes.UUIDV1,
        unique: true
      },
      name: {
        type: DataTypes.STRING,
        allowNull: false,
        defaultValue: null,
        validate: _.merge({ len: [1, 64] },
          is.subdomainable,
          is.unique({ attribute: 'name', scope: 'user' })
        )
      },
      encrypted_cert: {
        type: DataTypes.TEXT,
        allowNull: true,
        defaultValue: null
      },
      strategy: {
        type: DataTypes.STRING,
        allowNull: false,
        defaultValue: 'spread',
        validate: {
          isIn: {
            args: [['spread', 'binpack', 'random']],
            msg: 'Must be spread, binpack or random'
          }
        }
      },
      docker_version: {
        type: DataTypes.STRING,
        allowNull: false,
        defaultValue: config.latestVersions.docker,
      },
      swarm_version: {
        type: DataTypes.STRING,
        allowNull: false,
        defaultValue: config.latestVersions.swarm,
      },
    },
    options: {
      defaultScope: { order: [['id', 'ASC']] },
      scopes: {
        user: function(id) {
          return { where: { user_id: id } };
        },
        filtered: function(filters) {
          let criterias = _.pick(filters, ['name', 'strategy']);

          return { where: criterias };
        }
      },
      getterMethods: {
        state_message: function() {
          let state = this.get('state');

          switch (state) {
            case 'empty':
              return 'Create at least one node to work with this cluster';
            case 'unreachable':
              return 'Master node is unreachable';
            case 'deploying':
              return 'One or more node(s) are beeing deployed';
            case 'upgrading':
              return 'One or more node(s) are beeing upgraded';
            case 'updating':
              return 'One or more node(s) are beeing updated';
            case 'running':
              return 'Cluster is running and reachable';
          }
        }
      },
      hooks: {
        beforeCreate: function(cluster) {
          return Cert.generate().then(cert => {
            cluster.encryptCert(cert);
          });
        }
      },
      instanceMethods: {
        _lastStateFromNodes: function({ ignore, options }) {
          let criterias = {
            scope: 'nonRunningNorUnreachable',
            where: { id: { $ne: ignore.id } },
            attributes: ['last_state'],
            order: [['updated_at', 'DESC']],
            limit: 1
          };
          return this.getNodes(criterias, options).then(_.first).then(node => {
            return !node ? 'running' : node.last_state;
          });
        },
        adaptStateTo: function({ action, node, options, masterSwitch }) {
          let getLastState;

          switch (action) {
            case 'destroyed':
              if (this.nodes_count <= 1) {
                return this.update(
                  { last_state: 'empty', last_seen: null }, options
                );
              }
              getLastState = this._lastStateFromNodes({
                ignore:  node,
                options: options,
              });
              break;
            case 'notify':
              getLastState = this._lastStateFromNodes({
                ignore:  node,
                options: options,
              });
              break;
            default:
              getLastState = Promise.resolve(node.last_state);
          }
          return getLastState.then(lastState => {
            let changes = { last_state: lastState };

            if (
              (action === 'destroyed' && node.master) ||
              (masterSwitch && !node.master)
            ) {
              changes.last_seen = null;
            }
            if (masterSwitch && node.master) {
              changes.last_seen = node.last_seen;
            }
            return this.update(changes, options);
          });
        }
      },
      classMethods: {
        associate: function(models) {
          this.hasMany(models.Node, { counterCache: { as: 'nodes_count' } });
        }
      }
    }
  }, CONCERNS, { DataTypes: DataTypes });

  return sequelize.define('Cluster', attributes, options);
};