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