github.com/aminovpavel/nomad@v0.11.8/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 function filesForPath(allocFiles, filterPath) {
    15    return allocFiles.where(
    16      file =>
    17        (!filterPath || file.path.startsWith(filterPath)) &&
    18        file.path.length > filterPath.length &&
    19        !file.path.substr(filterPath.length + 1).includes('/')
    20    );
    21  }
    22  
    23  export default function() {
    24    this.timing = 0; // delay for each request, automatically set to 0 during testing
    25  
    26    this.logging = window.location.search.includes('mirage-logging=true');
    27  
    28    this.namespace = 'v1';
    29    this.trackRequests = Ember.testing;
    30  
    31    const nomadIndices = {}; // used for tracking blocking queries
    32    const server = this;
    33    const withBlockingSupport = function(fn) {
    34      return function(schema, request) {
    35        // Get the original response
    36        let { url } = request;
    37        url = url.replace(/index=\d+[&;]?/, '');
    38        const response = fn.apply(this, arguments);
    39  
    40        // Get and increment the appropriate index
    41        nomadIndices[url] || (nomadIndices[url] = 2);
    42        const index = nomadIndices[url];
    43        nomadIndices[url]++;
    44  
    45        // Annotate the response with the index
    46        if (response instanceof Response) {
    47          response.headers['X-Nomad-Index'] = index;
    48          return response;
    49        }
    50        return new Response(200, { 'x-nomad-index': index }, response);
    51      };
    52    };
    53  
    54    this.get(
    55      '/jobs',
    56      withBlockingSupport(function({ jobs }, { queryParams }) {
    57        const json = this.serialize(jobs.all());
    58        const namespace = queryParams.namespace || 'default';
    59        return json
    60          .filter(job =>
    61            namespace === 'default'
    62              ? !job.NamespaceID || job.NamespaceID === namespace
    63              : job.NamespaceID === namespace
    64          )
    65          .map(job => filterKeys(job, 'TaskGroups', 'NamespaceID'));
    66      })
    67    );
    68  
    69    this.post('/jobs', function(schema, req) {
    70      const body = JSON.parse(req.requestBody);
    71  
    72      if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload');
    73  
    74      return okEmpty();
    75    });
    76  
    77    this.post('/jobs/parse', function(schema, req) {
    78      const body = JSON.parse(req.requestBody);
    79  
    80      if (!body.JobHCL)
    81        return new Response(400, {}, 'JobHCL is a required field on the request payload');
    82      if (!body.Canonicalize) return new Response(400, {}, 'Expected Canonicalize to be true');
    83  
    84      // Parse the name out of the first real line of HCL to match IDs in the new job record
    85      // Regex expectation:
    86      //   in:  job "job-name" {
    87      //   out: job-name
    88      const nameFromHCLBlock = /.+?"(.+?)"/;
    89      const jobName = body.JobHCL.trim()
    90        .split('\n')[0]
    91        .match(nameFromHCLBlock)[1];
    92  
    93      const job = server.create('job', { id: jobName });
    94      return new Response(200, {}, this.serialize(job));
    95    });
    96  
    97    this.post('/job/:id/plan', function(schema, req) {
    98      const body = JSON.parse(req.requestBody);
    99  
   100      if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload');
   101      if (!body.Diff) return new Response(400, {}, 'Expected Diff to be true');
   102  
   103      const FailedTGAllocs = body.Job.Unschedulable && generateFailedTGAllocs(body.Job);
   104  
   105      return new Response(
   106        200,
   107        {},
   108        JSON.stringify({ FailedTGAllocs, Diff: generateDiff(req.params.id) })
   109      );
   110    });
   111  
   112    this.get(
   113      '/job/:id',
   114      withBlockingSupport(function({ jobs }, { params, queryParams }) {
   115        const job = jobs.all().models.find(job => {
   116          const jobIsDefault = !job.namespaceId || job.namespaceId === 'default';
   117          const qpIsDefault = !queryParams.namespace || queryParams.namespace === 'default';
   118          return (
   119            job.id === params.id &&
   120            (job.namespaceId === queryParams.namespace || (jobIsDefault && qpIsDefault))
   121          );
   122        });
   123  
   124        return job ? this.serialize(job) : new Response(404, {}, null);
   125      })
   126    );
   127  
   128    this.post('/job/:id', function(schema, req) {
   129      const body = JSON.parse(req.requestBody);
   130  
   131      if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload');
   132  
   133      return okEmpty();
   134    });
   135  
   136    this.get(
   137      '/job/:id/summary',
   138      withBlockingSupport(function({ jobSummaries }, { params }) {
   139        return this.serialize(jobSummaries.findBy({ jobId: params.id }));
   140      })
   141    );
   142  
   143    this.get('/job/:id/allocations', function({ allocations }, { params }) {
   144      return this.serialize(allocations.where({ jobId: params.id }));
   145    });
   146  
   147    this.get('/job/:id/versions', function({ jobVersions }, { params }) {
   148      return this.serialize(jobVersions.where({ jobId: params.id }));
   149    });
   150  
   151    this.get('/job/:id/deployments', function({ deployments }, { params }) {
   152      return this.serialize(deployments.where({ jobId: params.id }));
   153    });
   154  
   155    this.get('/job/:id/deployment', function({ deployments }, { params }) {
   156      const deployment = deployments.where({ jobId: params.id }).models[0];
   157      return deployment ? this.serialize(deployment) : new Response(200, {}, 'null');
   158    });
   159  
   160    this.post('/job/:id/periodic/force', function(schema, { params }) {
   161      // Create the child job
   162      const parent = schema.jobs.find(params.id);
   163  
   164      // Use the server instead of the schema to leverage the job factory
   165      server.create('job', 'periodicChild', {
   166        parentId: parent.id,
   167        namespaceId: parent.namespaceId,
   168        namespace: parent.namespace,
   169        createAllocations: parent.createAllocations,
   170      });
   171  
   172      return okEmpty();
   173    });
   174  
   175    this.delete('/job/:id', function(schema, { params }) {
   176      const job = schema.jobs.find(params.id);
   177      job.update({ status: 'dead' });
   178      return new Response(204, {}, '');
   179    });
   180  
   181    this.get('/deployment/:id');
   182    this.post('/deployment/promote/:id', function() {
   183      return new Response(204, {}, '');
   184    });
   185  
   186    this.get('/job/:id/evaluations', function({ evaluations }, { params }) {
   187      return this.serialize(evaluations.where({ jobId: params.id }));
   188    });
   189  
   190    this.get('/evaluation/:id');
   191  
   192    this.get('/deployment/allocations/:id', function(schema, { params }) {
   193      const job = schema.jobs.find(schema.deployments.find(params.id).jobId);
   194      const allocations = schema.allocations.where({ jobId: job.id });
   195  
   196      return this.serialize(allocations.slice(0, 3));
   197    });
   198  
   199    this.get('/nodes', function({ nodes }) {
   200      const json = this.serialize(nodes.all());
   201      return json;
   202    });
   203  
   204    this.get('/node/:id');
   205  
   206    this.get('/node/:id/allocations', function({ allocations }, { params }) {
   207      return this.serialize(allocations.where({ nodeId: params.id }));
   208    });
   209  
   210    this.post('/node/:id/eligibility', function({ nodes }, { params, requestBody }) {
   211      const body = JSON.parse(requestBody);
   212      const node = nodes.find(params.id);
   213  
   214      node.update({ schedulingEligibility: body.Elibility === 'eligible' });
   215      return this.serialize(node);
   216    });
   217  
   218    this.post('/node/:id/drain', function({ nodes }, { params }) {
   219      return this.serialize(nodes.find(params.id));
   220    });
   221  
   222    this.get('/allocations');
   223  
   224    this.get('/allocation/:id');
   225  
   226    this.post('/allocation/:id/stop', function() {
   227      return new Response(204, {}, '');
   228    });
   229  
   230    this.get(
   231      '/volumes',
   232      withBlockingSupport(function({ csiVolumes }, { queryParams }) {
   233        if (queryParams.type !== 'csi') {
   234          return new Response(200, {}, '[]');
   235        }
   236  
   237        const json = this.serialize(csiVolumes.all());
   238        const namespace = queryParams.namespace || 'default';
   239        return json.filter(volume =>
   240          namespace === 'default'
   241            ? !volume.NamespaceID || volume.NamespaceID === namespace
   242            : volume.NamespaceID === namespace
   243        );
   244      })
   245    );
   246  
   247    this.get(
   248      '/volume/:id',
   249      withBlockingSupport(function({ csiVolumes }, { params }) {
   250        if (!params.id.startsWith('csi/')) {
   251          return new Response(404, {}, null);
   252        }
   253  
   254        const id = params.id.replace(/^csi\//, '');
   255        const volume = csiVolumes.find(id);
   256  
   257        if (!volume) {
   258          return new Response(404, {}, null);
   259        }
   260  
   261        return this.serialize(volume);
   262      })
   263    );
   264  
   265    this.get('/plugins', function({ csiPlugins }, { queryParams }) {
   266      if (queryParams.type !== 'csi') {
   267        return new Response(200, {}, '[]');
   268      }
   269  
   270      return this.serialize(csiPlugins.all());
   271    });
   272  
   273    this.get('/plugin/:id', function({ csiPlugins }, { params }) {
   274      if (!params.id.startsWith('csi/')) {
   275        return new Response(404, {}, null);
   276      }
   277  
   278      const id = params.id.replace(/^csi\//, '');
   279      const volume = csiPlugins.find(id);
   280  
   281      if (!volume) {
   282        return new Response(404, {}, null);
   283      }
   284  
   285      return this.serialize(volume);
   286    });
   287  
   288    this.get('/namespaces', function({ namespaces }) {
   289      const records = namespaces.all();
   290  
   291      if (records.length) {
   292        return this.serialize(records);
   293      }
   294  
   295      return new Response(501, {}, null);
   296    });
   297  
   298    this.get('/namespace/:id', function({ namespaces }, { params }) {
   299      if (namespaces.all().length) {
   300        return this.serialize(namespaces.find(params.id));
   301      }
   302  
   303      return new Response(501, {}, null);
   304    });
   305  
   306    this.get('/agent/members', function({ agents, regions }) {
   307      const firstRegion = regions.first();
   308      return {
   309        ServerRegion: firstRegion ? firstRegion.id : null,
   310        Members: this.serialize(agents.all()),
   311      };
   312    });
   313  
   314    this.get('/status/leader', function(schema) {
   315      return JSON.stringify(findLeader(schema));
   316    });
   317  
   318    this.get('/acl/token/self', function({ tokens }, req) {
   319      const secret = req.requestHeaders['X-Nomad-Token'];
   320      const tokenForSecret = tokens.findBy({ secretId: secret });
   321  
   322      // Return the token if it exists
   323      if (tokenForSecret) {
   324        return this.serialize(tokenForSecret);
   325      }
   326  
   327      // Client error if it doesn't
   328      return new Response(400, {}, null);
   329    });
   330  
   331    this.get('/acl/token/:id', function({ tokens }, req) {
   332      const token = tokens.find(req.params.id);
   333      const secret = req.requestHeaders['X-Nomad-Token'];
   334      const tokenForSecret = tokens.findBy({ secretId: secret });
   335  
   336      // Return the token only if the request header matches the token
   337      // or the token is of type management
   338      if (token.secretId === secret || (tokenForSecret && tokenForSecret.type === 'management')) {
   339        return this.serialize(token);
   340      }
   341  
   342      // Return not authorized otherwise
   343      return new Response(403, {}, null);
   344    });
   345  
   346    this.get('/acl/policy/:id', function({ policies, tokens }, req) {
   347      const policy = policies.find(req.params.id);
   348      const secret = req.requestHeaders['X-Nomad-Token'];
   349      const tokenForSecret = tokens.findBy({ secretId: secret });
   350  
   351      if (req.params.id === 'anonymous') {
   352        if (policy) {
   353          return this.serialize(policy);
   354        } else {
   355          return new Response(404, {}, null);
   356        }
   357      }
   358  
   359      // Return the policy only if the token that matches the request header
   360      // includes the policy or if the token that matches the request header
   361      // is of type management
   362      if (
   363        tokenForSecret &&
   364        (tokenForSecret.policies.includes(policy) || tokenForSecret.type === 'management')
   365      ) {
   366        return this.serialize(policy);
   367      }
   368  
   369      // Return not authorized otherwise
   370      return new Response(403, {}, null);
   371    });
   372  
   373    this.get('/regions', function({ regions }) {
   374      return this.serialize(regions.all());
   375    });
   376  
   377    const clientAllocationStatsHandler = function({ clientAllocationStats }, { params }) {
   378      return this.serialize(clientAllocationStats.find(params.id));
   379    };
   380  
   381    const clientAllocationLog = function(server, { params, queryParams }) {
   382      const allocation = server.allocations.find(params.allocation_id);
   383      const tasks = allocation.taskStateIds.map(id => server.taskStates.find(id));
   384  
   385      if (!tasks.mapBy('name').includes(queryParams.task)) {
   386        return new Response(400, {}, 'must include task name');
   387      }
   388  
   389      if (queryParams.plain) {
   390        return logFrames.join('');
   391      }
   392  
   393      return logEncode(logFrames, logFrames.length - 1);
   394    };
   395  
   396    const clientAllocationFSLsHandler = function({ allocFiles }, { queryParams: { path } }) {
   397      const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path;
   398      const files = filesForPath(allocFiles, filterPath);
   399      return this.serialize(files);
   400    };
   401  
   402    const clientAllocationFSStatHandler = function({ allocFiles }, { queryParams: { path } }) {
   403      const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path;
   404  
   405      // Root path
   406      if (!filterPath) {
   407        return this.serialize({
   408          IsDir: true,
   409          ModTime: new Date(),
   410        });
   411      }
   412  
   413      // Either a file or a nested directory
   414      const file = allocFiles.where({ path: filterPath }).models[0];
   415      return this.serialize(file);
   416    };
   417  
   418    const clientAllocationCatHandler = function({ allocFiles }, { queryParams }) {
   419      const [file, err] = fileOrError(allocFiles, queryParams.path);
   420  
   421      if (err) return err;
   422      return file.body;
   423    };
   424  
   425    const clientAllocationStreamHandler = function({ allocFiles }, { queryParams }) {
   426      const [file, err] = fileOrError(allocFiles, queryParams.path);
   427  
   428      if (err) return err;
   429  
   430      // Pretender, and therefore Mirage, doesn't support streaming responses.
   431      return file.body;
   432    };
   433  
   434    const clientAllocationReadAtHandler = function({ allocFiles }, { queryParams }) {
   435      const [file, err] = fileOrError(allocFiles, queryParams.path);
   436  
   437      if (err) return err;
   438      return file.body.substr(queryParams.offset || 0, queryParams.limit);
   439    };
   440  
   441    const fileOrError = function(allocFiles, path, message = 'Operation not allowed on a directory') {
   442      // Root path
   443      if (path === '/') {
   444        return [null, new Response(400, {}, message)];
   445      }
   446  
   447      const file = allocFiles.where({ path }).models[0];
   448      if (file.isDir) {
   449        return [null, new Response(400, {}, message)];
   450      }
   451  
   452      return [file, null];
   453    };
   454  
   455    // Client requests are available on the server and the client
   456    this.put('/client/allocation/:id/restart', function() {
   457      return new Response(204, {}, '');
   458    });
   459  
   460    this.get('/client/allocation/:id/stats', clientAllocationStatsHandler);
   461    this.get('/client/fs/logs/:allocation_id', clientAllocationLog);
   462  
   463    this.get('/client/fs/ls/:allocation_id', clientAllocationFSLsHandler);
   464    this.get('/client/fs/stat/:allocation_id', clientAllocationFSStatHandler);
   465    this.get('/client/fs/cat/:allocation_id', clientAllocationCatHandler);
   466    this.get('/client/fs/stream/:allocation_id', clientAllocationStreamHandler);
   467    this.get('/client/fs/readat/:allocation_id', clientAllocationReadAtHandler);
   468  
   469    this.get('/client/stats', function({ clientStats }, { queryParams }) {
   470      const seed = faker.random.number(10);
   471      if (seed >= 8) {
   472        const stats = clientStats.find(queryParams.node_id);
   473        stats.update({
   474          timestamp: Date.now() * 1000000,
   475          CPUTicksConsumed: stats.CPUTicksConsumed + faker.random.number({ min: -10, max: 10 }),
   476        });
   477        return this.serialize(stats);
   478      } else {
   479        return new Response(500, {}, null);
   480      }
   481    });
   482  
   483    // TODO: in the future, this hack may be replaceable with dynamic host name
   484    // support in pretender: https://github.com/pretenderjs/pretender/issues/210
   485    HOSTS.forEach(host => {
   486      this.get(`http://${host}/v1/client/allocation/:id/stats`, clientAllocationStatsHandler);
   487      this.get(`http://${host}/v1/client/fs/logs/:allocation_id`, clientAllocationLog);
   488  
   489      this.get(`http://${host}/v1/client/fs/ls/:allocation_id`, clientAllocationFSLsHandler);
   490      this.get(`http://${host}/v1/client/stat/ls/:allocation_id`, clientAllocationFSStatHandler);
   491      this.get(`http://${host}/v1/client/fs/cat/:allocation_id`, clientAllocationCatHandler);
   492      this.get(`http://${host}/v1/client/fs/stream/:allocation_id`, clientAllocationStreamHandler);
   493      this.get(`http://${host}/v1/client/fs/readat/:allocation_id`, clientAllocationReadAtHandler);
   494  
   495      this.get(`http://${host}/v1/client/stats`, function({ clientStats }) {
   496        return this.serialize(clientStats.find(host));
   497      });
   498    });
   499  }
   500  
   501  function filterKeys(object, ...keys) {
   502    const clone = copy(object, true);
   503  
   504    keys.forEach(key => {
   505      delete clone[key];
   506    });
   507  
   508    return clone;
   509  }
   510  
   511  // An empty response but not a 204 No Content. This is still a valid JSON
   512  // response that represents a payload with no worthwhile data.
   513  function okEmpty() {
   514    return new Response(200, {}, '{}');
   515  }
   516  
   517  function generateFailedTGAllocs(job, taskGroups) {
   518    const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name');
   519  
   520    let tgNames = ['tg-one', 'tg-two'];
   521    if (taskGroupsFromSpec && taskGroupsFromSpec.length) tgNames = taskGroupsFromSpec;
   522    if (taskGroups && taskGroups.length) tgNames = taskGroups;
   523  
   524    return tgNames.reduce((hash, tgName) => {
   525      hash[tgName] = generateTaskGroupFailures();
   526      return hash;
   527    }, {});
   528  }