github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/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.get(
   161      '/job/:id/scale',
   162      withBlockingSupport(function({ jobScales }, { params }) {
   163        const obj = jobScales.findBy({ jobId: params.id });
   164        return this.serialize(jobScales.findBy({ jobId: params.id }));
   165      })
   166    );
   167  
   168    this.post('/job/:id/periodic/force', function(schema, { params }) {
   169      // Create the child job
   170      const parent = schema.jobs.find(params.id);
   171  
   172      // Use the server instead of the schema to leverage the job factory
   173      server.create('job', 'periodicChild', {
   174        parentId: parent.id,
   175        namespaceId: parent.namespaceId,
   176        namespace: parent.namespace,
   177        createAllocations: parent.createAllocations,
   178      });
   179  
   180      return okEmpty();
   181    });
   182  
   183    this.post('/job/:id/scale', function({ jobs }, { params }) {
   184      return this.serialize(jobs.find(params.id));
   185    });
   186  
   187    this.delete('/job/:id', function(schema, { params }) {
   188      const job = schema.jobs.find(params.id);
   189      job.update({ status: 'dead' });
   190      return new Response(204, {}, '');
   191    });
   192  
   193    this.get('/deployment/:id');
   194  
   195    this.post('/deployment/fail/:id', function() {
   196      return new Response(204, {}, '');
   197    });
   198  
   199    this.post('/deployment/promote/:id', function() {
   200      return new Response(204, {}, '');
   201    });
   202  
   203    this.get('/job/:id/evaluations', function({ evaluations }, { params }) {
   204      return this.serialize(evaluations.where({ jobId: params.id }));
   205    });
   206  
   207    this.get('/evaluation/:id');
   208  
   209    this.get('/deployment/allocations/:id', function(schema, { params }) {
   210      const job = schema.jobs.find(schema.deployments.find(params.id).jobId);
   211      const allocations = schema.allocations.where({ jobId: job.id });
   212  
   213      return this.serialize(allocations.slice(0, 3));
   214    });
   215  
   216    this.get('/nodes', function({ nodes }) {
   217      const json = this.serialize(nodes.all());
   218      return json;
   219    });
   220  
   221    this.get('/node/:id');
   222  
   223    this.get('/node/:id/allocations', function({ allocations }, { params }) {
   224      return this.serialize(allocations.where({ nodeId: params.id }));
   225    });
   226  
   227    this.post('/node/:id/eligibility', function({ nodes }, { params, requestBody }) {
   228      const body = JSON.parse(requestBody);
   229      const node = nodes.find(params.id);
   230  
   231      node.update({ schedulingEligibility: body.Elibility === 'eligible' });
   232      return this.serialize(node);
   233    });
   234  
   235    this.post('/node/:id/drain', function({ nodes }, { params }) {
   236      return this.serialize(nodes.find(params.id));
   237    });
   238  
   239    this.get('/allocations');
   240  
   241    this.get('/allocation/:id');
   242  
   243    this.post('/allocation/:id/stop', function() {
   244      return new Response(204, {}, '');
   245    });
   246  
   247    this.get(
   248      '/volumes',
   249      withBlockingSupport(function({ csiVolumes }, { queryParams }) {
   250        if (queryParams.type !== 'csi') {
   251          return new Response(200, {}, '[]');
   252        }
   253  
   254        const json = this.serialize(csiVolumes.all());
   255        const namespace = queryParams.namespace || 'default';
   256        return json.filter(volume =>
   257          namespace === 'default'
   258            ? !volume.NamespaceID || volume.NamespaceID === namespace
   259            : volume.NamespaceID === namespace
   260        );
   261      })
   262    );
   263  
   264    this.get(
   265      '/volume/:id',
   266      withBlockingSupport(function({ csiVolumes }, { params }) {
   267        if (!params.id.startsWith('csi/')) {
   268          return new Response(404, {}, null);
   269        }
   270  
   271        const id = params.id.replace(/^csi\//, '');
   272        const volume = csiVolumes.find(id);
   273  
   274        if (!volume) {
   275          return new Response(404, {}, null);
   276        }
   277  
   278        return this.serialize(volume);
   279      })
   280    );
   281  
   282    this.get('/plugins', function({ csiPlugins }, { queryParams }) {
   283      if (queryParams.type !== 'csi') {
   284        return new Response(200, {}, '[]');
   285      }
   286  
   287      return this.serialize(csiPlugins.all());
   288    });
   289  
   290    this.get('/plugin/:id', function({ csiPlugins }, { params }) {
   291      if (!params.id.startsWith('csi/')) {
   292        return new Response(404, {}, null);
   293      }
   294  
   295      const id = params.id.replace(/^csi\//, '');
   296      const volume = csiPlugins.find(id);
   297  
   298      if (!volume) {
   299        return new Response(404, {}, null);
   300      }
   301  
   302      return this.serialize(volume);
   303    });
   304  
   305    this.get('/namespaces', function({ namespaces }) {
   306      const records = namespaces.all();
   307  
   308      if (records.length) {
   309        return this.serialize(records);
   310      }
   311  
   312      return new Response(501, {}, null);
   313    });
   314  
   315    this.get('/namespace/:id', function({ namespaces }, { params }) {
   316      if (namespaces.all().length) {
   317        return this.serialize(namespaces.find(params.id));
   318      }
   319  
   320      return new Response(501, {}, null);
   321    });
   322  
   323    this.get('/agent/members', function({ agents, regions }) {
   324      const firstRegion = regions.first();
   325      return {
   326        ServerRegion: firstRegion ? firstRegion.id : null,
   327        Members: this.serialize(agents.all()),
   328      };
   329    });
   330  
   331    this.get('/agent/self', function({ agents }) {
   332      return {
   333        member: this.serialize(agents.first()),
   334      };
   335    });
   336  
   337    this.get('/agent/monitor', function({ agents, nodes }, { queryParams }) {
   338      const serverId = queryParams.server_id;
   339      const clientId = queryParams.client_id;
   340  
   341      if (serverId && clientId)
   342        return new Response(400, {}, 'specify a client or a server, not both');
   343      if (serverId && !agents.findBy({ name: serverId }))
   344        return new Response(400, {}, 'specified server does not exist');
   345      if (clientId && !nodes.find(clientId))
   346        return new Response(400, {}, 'specified client does not exist');
   347  
   348      if (queryParams.plain) {
   349        return logFrames.join('');
   350      }
   351  
   352      return logEncode(logFrames, logFrames.length - 1);
   353    });
   354  
   355    this.get('/status/leader', function(schema) {
   356      return JSON.stringify(findLeader(schema));
   357    });
   358  
   359    this.get('/acl/token/self', function({ tokens }, req) {
   360      const secret = req.requestHeaders['X-Nomad-Token'];
   361      const tokenForSecret = tokens.findBy({ secretId: secret });
   362  
   363      // Return the token if it exists
   364      if (tokenForSecret) {
   365        return this.serialize(tokenForSecret);
   366      }
   367  
   368      // Client error if it doesn't
   369      return new Response(400, {}, null);
   370    });
   371  
   372    this.get('/acl/token/:id', function({ tokens }, req) {
   373      const token = tokens.find(req.params.id);
   374      const secret = req.requestHeaders['X-Nomad-Token'];
   375      const tokenForSecret = tokens.findBy({ secretId: secret });
   376  
   377      // Return the token only if the request header matches the token
   378      // or the token is of type management
   379      if (token.secretId === secret || (tokenForSecret && tokenForSecret.type === 'management')) {
   380        return this.serialize(token);
   381      }
   382  
   383      // Return not authorized otherwise
   384      return new Response(403, {}, null);
   385    });
   386  
   387    this.get('/acl/policy/:id', function({ policies, tokens }, req) {
   388      const policy = policies.find(req.params.id);
   389      const secret = req.requestHeaders['X-Nomad-Token'];
   390      const tokenForSecret = tokens.findBy({ secretId: secret });
   391  
   392      if (req.params.id === 'anonymous') {
   393        if (policy) {
   394          return this.serialize(policy);
   395        } else {
   396          return new Response(404, {}, null);
   397        }
   398      }
   399  
   400      // Return the policy only if the token that matches the request header
   401      // includes the policy or if the token that matches the request header
   402      // is of type management
   403      if (
   404        tokenForSecret &&
   405        (tokenForSecret.policies.includes(policy) || tokenForSecret.type === 'management')
   406      ) {
   407        return this.serialize(policy);
   408      }
   409  
   410      // Return not authorized otherwise
   411      return new Response(403, {}, null);
   412    });
   413  
   414    this.get('/regions', function({ regions }) {
   415      return this.serialize(regions.all());
   416    });
   417  
   418    this.get('/operator/license', function({ features }) {
   419      const records = features.all();
   420  
   421      if (records.length) {
   422        return {
   423          License: {
   424            Features: records.models.mapBy('name'),
   425          }
   426        };
   427      }
   428  
   429      return new Response(501, {}, null);
   430    });
   431  
   432    const clientAllocationStatsHandler = function({ clientAllocationStats }, { params }) {
   433      return this.serialize(clientAllocationStats.find(params.id));
   434    };
   435  
   436    const clientAllocationLog = function(server, { params, queryParams }) {
   437      const allocation = server.allocations.find(params.allocation_id);
   438      const tasks = allocation.taskStateIds.map(id => server.taskStates.find(id));
   439  
   440      if (!tasks.mapBy('name').includes(queryParams.task)) {
   441        return new Response(400, {}, 'must include task name');
   442      }
   443  
   444      if (queryParams.plain) {
   445        return logFrames.join('');
   446      }
   447  
   448      return logEncode(logFrames, logFrames.length - 1);
   449    };
   450  
   451    const clientAllocationFSLsHandler = function({ allocFiles }, { queryParams: { path } }) {
   452      const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path;
   453      const files = filesForPath(allocFiles, filterPath);
   454      return this.serialize(files);
   455    };
   456  
   457    const clientAllocationFSStatHandler = function({ allocFiles }, { queryParams: { path } }) {
   458      const filterPath = path.endsWith('/') ? path.substr(0, path.length - 1) : path;
   459  
   460      // Root path
   461      if (!filterPath) {
   462        return this.serialize({
   463          IsDir: true,
   464          ModTime: new Date(),
   465        });
   466      }
   467  
   468      // Either a file or a nested directory
   469      const file = allocFiles.where({ path: filterPath }).models[0];
   470      return this.serialize(file);
   471    };
   472  
   473    const clientAllocationCatHandler = function({ allocFiles }, { queryParams }) {
   474      const [file, err] = fileOrError(allocFiles, queryParams.path);
   475  
   476      if (err) return err;
   477      return file.body;
   478    };
   479  
   480    const clientAllocationStreamHandler = function({ allocFiles }, { queryParams }) {
   481      const [file, err] = fileOrError(allocFiles, queryParams.path);
   482  
   483      if (err) return err;
   484  
   485      // Pretender, and therefore Mirage, doesn't support streaming responses.
   486      return file.body;
   487    };
   488  
   489    const clientAllocationReadAtHandler = function({ allocFiles }, { queryParams }) {
   490      const [file, err] = fileOrError(allocFiles, queryParams.path);
   491  
   492      if (err) return err;
   493      return file.body.substr(queryParams.offset || 0, queryParams.limit);
   494    };
   495  
   496    const fileOrError = function(allocFiles, path, message = 'Operation not allowed on a directory') {
   497      // Root path
   498      if (path === '/') {
   499        return [null, new Response(400, {}, message)];
   500      }
   501  
   502      const file = allocFiles.where({ path }).models[0];
   503      if (file.isDir) {
   504        return [null, new Response(400, {}, message)];
   505      }
   506  
   507      return [file, null];
   508    };
   509  
   510    // Client requests are available on the server and the client
   511    this.put('/client/allocation/:id/restart', function() {
   512      return new Response(204, {}, '');
   513    });
   514  
   515    this.get('/client/allocation/:id/stats', clientAllocationStatsHandler);
   516    this.get('/client/fs/logs/:allocation_id', clientAllocationLog);
   517  
   518    this.get('/client/fs/ls/:allocation_id', clientAllocationFSLsHandler);
   519    this.get('/client/fs/stat/:allocation_id', clientAllocationFSStatHandler);
   520    this.get('/client/fs/cat/:allocation_id', clientAllocationCatHandler);
   521    this.get('/client/fs/stream/:allocation_id', clientAllocationStreamHandler);
   522    this.get('/client/fs/readat/:allocation_id', clientAllocationReadAtHandler);
   523  
   524    this.get('/client/stats', function({ clientStats }, { queryParams }) {
   525      const seed = faker.random.number(10);
   526      if (seed >= 8) {
   527        const stats = clientStats.find(queryParams.node_id);
   528        stats.update({
   529          timestamp: Date.now() * 1000000,
   530          CPUTicksConsumed: stats.CPUTicksConsumed + faker.random.number({ min: -10, max: 10 }),
   531        });
   532        return this.serialize(stats);
   533      } else {
   534        return new Response(500, {}, null);
   535      }
   536    });
   537  
   538    // TODO: in the future, this hack may be replaceable with dynamic host name
   539    // support in pretender: https://github.com/pretenderjs/pretender/issues/210
   540    HOSTS.forEach(host => {
   541      this.get(`http://${host}/v1/client/allocation/:id/stats`, clientAllocationStatsHandler);
   542      this.get(`http://${host}/v1/client/fs/logs/:allocation_id`, clientAllocationLog);
   543  
   544      this.get(`http://${host}/v1/client/fs/ls/:allocation_id`, clientAllocationFSLsHandler);
   545      this.get(`http://${host}/v1/client/stat/ls/:allocation_id`, clientAllocationFSStatHandler);
   546      this.get(`http://${host}/v1/client/fs/cat/:allocation_id`, clientAllocationCatHandler);
   547      this.get(`http://${host}/v1/client/fs/stream/:allocation_id`, clientAllocationStreamHandler);
   548      this.get(`http://${host}/v1/client/fs/readat/:allocation_id`, clientAllocationReadAtHandler);
   549  
   550      this.get(`http://${host}/v1/client/stats`, function({ clientStats }) {
   551        return this.serialize(clientStats.find(host));
   552      });
   553    });
   554  
   555    this.get('/recommendations', function(
   556      { jobs, namespaces, recommendations },
   557      { queryParams: { job: id, namespace } }
   558    ) {
   559      if (id) {
   560        if (!namespaces.all().length) {
   561          namespace = null;
   562        }
   563  
   564        const job = jobs.findBy({ id, namespace });
   565  
   566        if (!job) {
   567          return [];
   568        }
   569  
   570        const taskGroups = job.taskGroups.models;
   571  
   572        const tasks = taskGroups.reduce((tasks, taskGroup) => {
   573          return tasks.concat(taskGroup.tasks.models);
   574        }, []);
   575  
   576        const recommendationIds = tasks.reduce((recommendationIds, task) => {
   577          return recommendationIds.concat(task.recommendations.models.mapBy('id'));
   578        }, []);
   579  
   580        return recommendations.find(recommendationIds);
   581      } else {
   582        return recommendations.all();
   583      }
   584    });
   585  
   586    this.post('/recommendations/apply', function({ recommendations }, { requestBody }) {
   587      const { Apply, Dismiss } = JSON.parse(requestBody);
   588  
   589      Apply.concat(Dismiss).forEach(id => {
   590        const recommendation = recommendations.find(id);
   591        const task = recommendation.task;
   592  
   593        if (Apply.includes(id)) {
   594          task.resources[recommendation.resource] = recommendation.value;
   595        }
   596        recommendation.destroy();
   597        task.save();
   598      });
   599  
   600      return {};
   601    });
   602  }
   603  
   604  function filterKeys(object, ...keys) {
   605    const clone = copy(object, true);
   606  
   607    keys.forEach(key => {
   608      delete clone[key];
   609    });
   610  
   611    return clone;
   612  }
   613  
   614  // An empty response but not a 204 No Content. This is still a valid JSON
   615  // response that represents a payload with no worthwhile data.
   616  function okEmpty() {
   617    return new Response(200, {}, '{}');
   618  }
   619  
   620  function generateFailedTGAllocs(job, taskGroups) {
   621    const taskGroupsFromSpec = job.TaskGroups && job.TaskGroups.mapBy('Name');
   622  
   623    let tgNames = ['tg-one', 'tg-two'];
   624    if (taskGroupsFromSpec && taskGroupsFromSpec.length) tgNames = taskGroupsFromSpec;
   625    if (taskGroups && taskGroups.length) tgNames = taskGroups;
   626  
   627    return tgNames.reduce((hash, tgName) => {
   628      hash[tgName] = generateTaskGroupFailures();
   629      return hash;
   630    }, {});
   631  }