github.com/ferranbt/nomad@v0.9.3-0.20190607002617-85c449b7667c/ui/mirage/config.js (about)

     1  import Ember from 'ember';
     2  import Response from 'ember-cli-mirage/response';
     3  import { HOSTS } from './common';
     4  import { logFrames, logEncode } from './data/logs';
     5  import { generateDiff } from './factories/job-version';
     6  import { generateTaskGroupFailures } from './factories/evaluation';
     7  import { copy } from 'ember-copy';
     8  
     9  export function findLeader(schema) {
    10    const agent = schema.agents.first();
    11    return `${agent.address}:${agent.tags.port}`;
    12  }
    13  
    14  export default function() {
    15    this.timing = 0; // delay for each request, automatically set to 0 during testing
    16  
    17    this.namespace = 'v1';
    18    this.trackRequests = Ember.testing;
    19  
    20    const nomadIndices = {}; // used for tracking blocking queries
    21    const server = this;
    22    const withBlockingSupport = function(fn) {
    23      return function(schema, request) {
    24        // Get the original response
    25        let { url } = request;
    26        url = url.replace(/index=\d+[&;]?/, '');
    27        const response = fn.apply(this, arguments);
    28  
    29        // Get and increment the appropriate index
    30        nomadIndices[url] || (nomadIndices[url] = 2);
    31        const index = nomadIndices[url];
    32        nomadIndices[url]++;
    33  
    34        // Annotate the response with the index
    35        if (response instanceof Response) {
    36          response.headers['X-Nomad-Index'] = index;
    37          return response;
    38        }
    39        return new Response(200, { 'x-nomad-index': index }, response);
    40      };
    41    };
    42  
    43    this.get(
    44      '/jobs',
    45      withBlockingSupport(function({ jobs }, { queryParams }) {
    46        const json = this.serialize(jobs.all());
    47        const namespace = queryParams.namespace || 'default';
    48        return json
    49          .filter(job =>
    50            namespace === 'default'
    51              ? !job.NamespaceID || job.NamespaceID === namespace
    52              : job.NamespaceID === namespace
    53          )
    54          .map(job => filterKeys(job, 'TaskGroups', 'NamespaceID'));
    55      })
    56    );
    57  
    58    this.post('/jobs', function(schema, req) {
    59      const body = JSON.parse(req.requestBody);
    60  
    61      if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload');
    62  
    63      return okEmpty();
    64    });
    65  
    66    this.post('/jobs/parse', function(schema, req) {
    67      const body = JSON.parse(req.requestBody);
    68  
    69      if (!body.JobHCL)
    70        return new Response(400, {}, 'JobHCL is a required field on the request payload');
    71      if (!body.Canonicalize) return new Response(400, {}, 'Expected Canonicalize to be true');
    72  
    73      // Parse the name out of the first real line of HCL to match IDs in the new job record
    74      // Regex expectation:
    75      //   in:  job "job-name" {
    76      //   out: job-name
    77      const nameFromHCLBlock = /.+?"(.+?)"/;
    78      const jobName = body.JobHCL.trim()
    79        .split('\n')[0]
    80        .match(nameFromHCLBlock)[1];
    81  
    82      const job = server.create('job', { id: jobName });
    83      return new Response(200, {}, this.serialize(job));
    84    });
    85  
    86    this.post('/job/:id/plan', function(schema, req) {
    87      const body = JSON.parse(req.requestBody);
    88  
    89      if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload');
    90      if (!body.Diff) return new Response(400, {}, 'Expected Diff to be true');
    91  
    92      const FailedTGAllocs = body.Job.Unschedulable && generateFailedTGAllocs(body.Job);
    93  
    94      return new Response(
    95        200,
    96        {},
    97        JSON.stringify({ FailedTGAllocs, Diff: generateDiff(req.params.id) })
    98      );
    99    });
   100  
   101    this.get(
   102      '/job/:id',
   103      withBlockingSupport(function({ jobs }, { params, queryParams }) {
   104        const job = jobs.all().models.find(job => {
   105          const jobIsDefault = !job.namespaceId || job.namespaceId === 'default';
   106          const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default';
   107          return (
   108            job.id === params.id &&
   109            (job.namespaceId === queryParams.namespace || (jobIsDefault && qpIsDefault))
   110          );
   111        });
   112  
   113        return job ? this.serialize(job) : new Response(404, {}, null);
   114      })
   115    );
   116  
   117    this.post('/job/:id', function(schema, req) {
   118      const body = JSON.parse(req.requestBody);
   119  
   120      if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload');
   121  
   122      return okEmpty();
   123    });
   124  
   125    this.get(
   126      '/job/:id/summary',
   127      withBlockingSupport(function({ jobSummaries }, { params }) {
   128        return this.serialize(jobSummaries.findBy({ jobId: params.id }));
   129      })
   130    );
   131  
   132    this.get('/job/:id/allocations', function({ allocations }, { params }) {
   133      return this.serialize(allocations.where({ jobId: params.id }));
   134    });
   135  
   136    this.get('/job/:id/versions', function({ jobVersions }, { params }) {
   137      return this.serialize(jobVersions.where({ jobId: params.id }));
   138    });
   139  
   140    this.get('/job/:id/deployments', function({ deployments }, { params }) {
   141      return this.serialize(deployments.where({ jobId: params.id }));
   142    });
   143  
   144    this.get('/job/:id/deployment', function({ deployments }, { params }) {
   145      const deployment = deployments.where({ jobId: params.id }).models[0];
   146      return deployment ? this.serialize(deployment) : new Response(200, {}, 'null');
   147    });
   148  
   149    this.post('/job/:id/periodic/force', function(schema, { params }) {
   150      // Create the child job
   151      const parent = schema.jobs.find(params.id);
   152  
   153      // Use the server instead of the schema to leverage the job factory
   154      server.create('job', 'periodicChild', {
   155        parentId: parent.id,
   156        namespaceId: parent.namespaceId,
   157        namespace: parent.namespace,
   158        createAllocations: parent.createAllocations,
   159      });
   160  
   161      return okEmpty();
   162    });
   163  
   164    this.delete('/job/:id', function(schema, { params }) {
   165      const job = schema.jobs.find(params.id);
   166      job.update({ status: 'dead' });
   167      return new Response(204, {}, '');
   168    });
   169  
   170    this.get('/deployment/:id');
   171    this.post('/deployment/promote/:id', function() {
   172      return new Response(204, {}, '');
   173    });
   174  
   175    this.get('/job/:id/evaluations', function({ evaluations }, { params }) {
   176      return this.serialize(evaluations.where({ jobId: params.id }));
   177    });
   178  
   179    this.get('/evaluation/:id');
   180  
   181    this.get('/deployment/allocations/:id', function(schema, { params }) {
   182      const job = schema.jobs.find(schema.deployments.find(params.id).jobId);
   183      const allocations = schema.allocations.where({ jobId: job.id });
   184  
   185      return this.serialize(allocations.slice(0, 3));
   186    });
   187  
   188    this.get('/nodes', function({ nodes }) {
   189      const json = this.serialize(nodes.all());
   190      return json;
   191    });
   192  
   193    this.get('/node/:id');
   194  
   195    this.get('/node/:id/allocations', function({ allocations }, { params }) {
   196      return this.serialize(allocations.where({ nodeId: params.id }));
   197    });
   198  
   199    this.get('/allocations');
   200  
   201    this.get('/allocation/:id');
   202  
   203    this.post('/allocation/:id/stop', function() {
   204      return new Response(204, {}, '');
   205    });
   206  
   207    this.get('/namespaces', function({ namespaces }) {
   208      const records = namespaces.all();
   209  
   210      if (records.length) {
   211        return this.serialize(records);
   212      }
   213  
   214      return new Response(501, {}, null);
   215    });
   216  
   217    this.get('/namespace/:id', function({ namespaces }, { params }) {
   218      if (namespaces.all().length) {
   219        return this.serialize(namespaces.find(params.id));
   220      }
   221  
   222      return new Response(501, {}, null);
   223    });
   224  
   225    this.get('/agent/members', function({ agents, regions }) {
   226      const firstRegion = regions.first();
   227      return {
   228        ServerRegion: firstRegion ? firstRegion.id : null,
   229        Members: this.serialize(agents.all()),
   230      };
   231    });
   232  
   233    this.get('/status/leader', function(schema) {
   234      return JSON.stringify(findLeader(schema));
   235    });
   236  
   237    this.get('/acl/token/self', function({ tokens }, req) {
   238      const secret = req.requestHeaders['X-Nomad-Token'];
   239      const tokenForSecret = tokens.findBy({ secretId: secret });
   240  
   241      // Return the token if it exists
   242      if (tokenForSecret) {
   243        return this.serialize(tokenForSecret);
   244      }
   245  
   246      // Client error if it doesn't
   247      return new Response(400, {}, null);
   248    });
   249  
   250    this.get('/acl/token/:id', function({ tokens }, req) {
   251      const token = tokens.find(req.params.id);
   252      const secret = req.requestHeaders['X-Nomad-Token'];
   253      const tokenForSecret = tokens.findBy({ secretId: secret });
   254  
   255      // Return the token only if the request header matches the token
   256      // or the token is of type management
   257      if (token.secretId === secret || (tokenForSecret && tokenForSecret.type === 'management')) {
   258        return this.serialize(token);
   259      }
   260  
   261      // Return not authorized otherwise
   262      return new Response(403, {}, null);
   263    });
   264  
   265    this.get('/acl/policy/:id', function({ policies, tokens }, req) {
   266      const policy = policies.find(req.params.id);
   267      const secret = req.requestHeaders['X-Nomad-Token'];
   268      const tokenForSecret = tokens.findBy({ secretId: secret });
   269  
   270      // Return the policy only if the token that matches the request header
   271      // includes the policy or if the token that matches the request header
   272      // is of type management
   273      if (
   274        tokenForSecret &&
   275        (tokenForSecret.policies.includes(policy) || tokenForSecret.type === 'management')
   276      ) {
   277        return this.serialize(policy);
   278      }
   279  
   280      // Return not authorized otherwise
   281      return new Response(403, {}, null);
   282    });
   283  
   284    this.get('/regions', function({ regions }) {
   285      return this.serialize(regions.all());
   286    });
   287  
   288    const clientAllocationStatsHandler = function({ clientAllocationStats }, { params }) {
   289      return this.serialize(clientAllocationStats.find(params.id));
   290    };
   291  
   292    const clientAllocationLog = function(server, { params, queryParams }) {
   293      const allocation = server.allocations.find(params.allocation_id);
   294      const tasks = allocation.taskStateIds.map(id => server.taskStates.find(id));
   295  
   296      if (!tasks.mapBy('name').includes(queryParams.task)) {
   297        return new Response(400, {}, 'must include task name');
   298      }
   299  
   300      if (queryParams.plain) {
   301        return logFrames.join('');
   302      }
   303  
   304      return logEncode(logFrames, logFrames.length - 1);
   305    };
   306  
   307    // Client requests are available on the server and the client
   308    this.put('/client/allocation/:id/restart', function() {
   309      return new Response(204, {}, '');
   310    });
   311  
   312    this.get('/client/allocation/:id/stats', clientAllocationStatsHandler);
   313    this.get('/client/fs/logs/:allocation_id', clientAllocationLog);
   314  
   315    this.get('/client/stats', function({ clientStats }, { queryParams }) {
   316      const seed = Math.random();
   317      if (seed > 0.8) {
   318        const stats = clientStats.find(queryParams.node_id);
   319        stats.update({
   320          timestamp: Date.now() * 1000000,
   321          CPUTicksConsumed: stats.CPUTicksConsumed + (Math.random() * 20 - 10),
   322        });
   323        return this.serialize(stats);
   324      } else {
   325        return new Response(500, {}, null);
   326      }
   327    });
   328  
   329    // TODO: in the future, this hack may be replaceable with dynamic host name
   330    // support in pretender: https://github.com/pretenderjs/pretender/issues/210
   331    HOSTS.forEach(host => {
   332      this.get(`http://${host}/v1/client/allocation/:id/stats`, clientAllocationStatsHandler);
   333      this.get(`http://${host}/v1/client/fs/logs/:allocation_id`, clientAllocationLog);
   334  
   335      this.get(`http://${host}/v1/client/stats`, function({ clientStats }) {
   336        return this.serialize(clientStats.find(host));
   337      });
   338    });
   339  }
   340  
   341  function filterKeys(object, ...keys) {
   342    const clone = copy(object, true);
   343  
   344    keys.forEach(key => {
   345      delete clone[key];
   346    });
   347  
   348    return clone;
   349  }
   350  
   351  // An empty response but not a 204 No Content. This is still a valid JSON
   352  // response that represents a payload with no worthwhile data.
   353  function okEmpty() {
   354    return new Response(200, {}, '{}');
   355  }
   356  
   357  function generateFailedTGAllocs(job, taskGroups) {
   358    const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name');
   359  
   360    let tgNames = ['tg-one', 'tg-two'];
   361    if (taskGroupsFromSpec && taskGroupsFromSpec.length) tgNames = taskGroupsFromSpec;
   362    if (taskGroups && taskGroups.length) tgNames = taskGroups;
   363  
   364    return tgNames.reduce((hash, tgName) => {
   365      hash[tgName] = generateTaskGroupFailures();
   366      return hash;
   367    }, {});
   368  }