github.com/aminovpavel/nomad@v0.11.8/ui/tests/acceptance/exec-test.js (about)

     1  import { module, skip, test } from 'qunit';
     2  import { currentURL, settled } from '@ember/test-helpers';
     3  import { setupApplicationTest } from 'ember-qunit';
     4  import { setupMirage } from 'ember-cli-mirage/test-support';
     5  import Service from '@ember/service';
     6  import Exec from 'nomad-ui/tests/pages/exec';
     7  import KEYS from 'nomad-ui/utils/keys';
     8  
     9  module('Acceptance | exec', function(hooks) {
    10    setupApplicationTest(hooks);
    11    setupMirage(hooks);
    12  
    13    hooks.beforeEach(async function() {
    14      window.localStorage.clear();
    15      window.sessionStorage.clear();
    16  
    17      server.create('agent');
    18      server.create('node');
    19  
    20      this.job = server.create('job', {
    21        groupsCount: 2,
    22        groupTaskCount: 5,
    23        createAllocations: false,
    24        status: 'running',
    25      });
    26  
    27      this.job.task_group_ids.forEach(taskGroupId => {
    28        server.create('allocation', {
    29          jobId: this.job.id,
    30          taskGroup: server.db.taskGroups.find(taskGroupId).name,
    31          forceRunningClientStatus: true,
    32        });
    33      });
    34    });
    35  
    36    test('/exec/:job should show the region, namespace, and job name', async function(assert) {
    37      server.create('namespace');
    38      let namespace = server.create('namespace');
    39  
    40      server.create('region', { id: 'global' });
    41      server.create('region', { id: 'region-2' });
    42  
    43      this.job = server.create('job', {
    44        createAllocations: false,
    45        namespaceId: namespace.id,
    46        status: 'running',
    47      });
    48  
    49      await Exec.visitJob({ job: this.job.id, namespace: namespace.id, region: 'region-2' });
    50  
    51      assert.equal(document.title, 'Exec - region-2 - Nomad');
    52  
    53      assert.equal(Exec.header.region.text, this.job.region);
    54      assert.equal(Exec.header.namespace.text, this.job.namespace);
    55      assert.equal(Exec.header.job, this.job.name);
    56  
    57      assert.notOk(Exec.jobDead.isPresent);
    58    });
    59  
    60    test('/exec/:job should not show region and namespace when there are none', async function(assert) {
    61      await Exec.visitJob({ job: this.job.id });
    62  
    63      assert.ok(Exec.header.region.isHidden);
    64      assert.ok(Exec.header.namespace.isHidden);
    65    });
    66  
    67    test('/exec/:job should show the task groups collapsed by default and allow the tasks to be shown', async function(assert) {
    68      const firstTaskGroup = this.job.task_groups.models.sortBy('name')[0];
    69      await Exec.visitJob({ job: this.job.id });
    70  
    71      assert.equal(Exec.taskGroups.length, this.job.task_groups.length);
    72  
    73      assert.equal(Exec.taskGroups[0].name, firstTaskGroup.name);
    74      assert.equal(Exec.taskGroups[0].tasks.length, 0);
    75      assert.ok(Exec.taskGroups[0].chevron.isRight);
    76      assert.notOk(Exec.taskGroups[0].isLoading);
    77  
    78      await Exec.taskGroups[0].click();
    79      assert.equal(Exec.taskGroups[0].tasks.length, firstTaskGroup.tasks.length);
    80      assert.notOk(Exec.taskGroups[0].tasks[0].isActive);
    81      assert.ok(Exec.taskGroups[0].chevron.isDown);
    82  
    83      await Exec.taskGroups[0].click();
    84      assert.equal(Exec.taskGroups[0].tasks.length, 0);
    85    });
    86  
    87    test('/exec/:job should require selecting a task', async function(assert) {
    88      await Exec.visitJob({ job: this.job.id });
    89  
    90      assert.equal(
    91        window.execTerminal.buffer
    92          .getLine(0)
    93          .translateToString()
    94          .trim(),
    95        'Select a task to start your session.'
    96      );
    97    });
    98  
    99    test('a task group with a pending allocation shows a loading spinner', async function(assert) {
   100      let taskGroup = this.job.task_groups.models.sortBy('name')[0];
   101      this.server.db.allocations.update({ taskGroup: taskGroup.name }, { clientStatus: 'pending' });
   102  
   103      await Exec.visitJob({ job: this.job.id });
   104      assert.ok(Exec.taskGroups[0].isLoading);
   105    });
   106  
   107    test('a task group with no running task states or pending allocations should not be shown', async function(assert) {
   108      let taskGroup = this.job.task_groups.models.sortBy('name')[0];
   109      this.server.db.allocations.update({ taskGroup: taskGroup.name }, { clientStatus: 'failed' });
   110  
   111      await Exec.visitJob({ job: this.job.id });
   112      assert.notEqual(Exec.taskGroups[0].name, taskGroup.name);
   113    });
   114  
   115    test('an inactive task should not be shown', async function(assert) {
   116      let notRunningTaskGroup = this.job.task_groups.models.sortBy('name')[0];
   117      this.server.db.allocations.update(
   118        { taskGroup: notRunningTaskGroup.name },
   119        { clientStatus: 'failed' }
   120      );
   121  
   122      let runningTaskGroup = this.job.task_groups.models.sortBy('name')[1];
   123      runningTaskGroup.tasks.models.forEach((task, index) => {
   124        if (index > 0) {
   125          this.server.db.taskStates.update({ name: task.name }, { finishedAt: new Date() });
   126        }
   127      });
   128  
   129      await Exec.visitJob({ job: this.job.id });
   130      await Exec.taskGroups[0].click();
   131  
   132      assert.equal(Exec.taskGroups[0].tasks.length, 1);
   133    });
   134  
   135    test('a task that becomes active should appear', async function(assert) {
   136      let notRunningTaskGroup = this.job.task_groups.models.sortBy('name')[0];
   137      this.server.db.allocations.update(
   138        { taskGroup: notRunningTaskGroup.name },
   139        { clientStatus: 'failed' }
   140      );
   141  
   142      let runningTaskGroup = this.job.task_groups.models.sortBy('name')[1];
   143      let changingTaskStateName;
   144      runningTaskGroup.tasks.models.sortBy('name').forEach((task, index) => {
   145        if (index > 0) {
   146          this.server.db.taskStates.update({ name: task.name }, { finishedAt: new Date() });
   147        }
   148  
   149        if (index === 1) {
   150          changingTaskStateName = task.name;
   151        }
   152      });
   153  
   154      await Exec.visitJob({ job: this.job.id });
   155      await Exec.taskGroups[0].click();
   156  
   157      assert.equal(Exec.taskGroups[0].tasks.length, 1);
   158  
   159      // Approximate new task arrival via polling by changing a finished task state to be not finished
   160      this.owner
   161        .lookup('service:store')
   162        .peekAll('allocation')
   163        .forEach(allocation => {
   164          const changingTaskState = allocation.states.findBy('name', changingTaskStateName);
   165  
   166          if (changingTaskState) {
   167            changingTaskState.set('finishedAt', undefined);
   168          }
   169        });
   170  
   171      await settled();
   172  
   173      assert.equal(Exec.taskGroups[0].tasks.length, 2);
   174      assert.equal(Exec.taskGroups[0].tasks[1].name, changingTaskStateName);
   175    });
   176  
   177    test('a dead job has an inert window', async function(assert) {
   178      this.job.status = 'dead';
   179      this.job.save();
   180  
   181      let taskGroup = this.job.task_groups.models.sortBy('name')[0];
   182      let task = taskGroup.tasks.models.sortBy('name')[0];
   183  
   184      this.server.db.taskStates.update({ finishedAt: new Date() });
   185  
   186      await Exec.visitTask({
   187        job: this.job.id,
   188        task_group: taskGroup.name,
   189        task_name: task.name,
   190      });
   191  
   192      assert.ok(Exec.jobDead.isPresent);
   193      assert.equal(
   194        Exec.jobDead.message,
   195        `Job ${this.job.name} is dead and cannot host an exec session.`
   196      );
   197    });
   198  
   199    test('when a job dies the exec window becomes inert', async function(assert) {
   200      await Exec.visitJob({ job: this.job.id });
   201  
   202      // Approximate live-polling job death
   203      this.owner
   204        .lookup('service:store')
   205        .peekAll('job')
   206        .forEach(job => job.set('status', 'dead'));
   207  
   208      await settled();
   209  
   210      assert.ok(Exec.jobDead.isPresent);
   211    });
   212  
   213    test('visiting a path with a task group should open the group by default', async function(assert) {
   214      let taskGroup = this.job.task_groups.models.sortBy('name')[0];
   215      await Exec.visitTaskGroup({ job: this.job.id, task_group: taskGroup.name });
   216  
   217      assert.equal(Exec.taskGroups[0].tasks.length, taskGroup.tasks.length);
   218      assert.ok(Exec.taskGroups[0].chevron.isDown);
   219  
   220      let task = taskGroup.tasks.models.sortBy('name')[0];
   221      await Exec.visitTask({ job: this.job.id, task_group: taskGroup.name, task_name: task.name });
   222  
   223      assert.equal(Exec.taskGroups[0].tasks.length, taskGroup.tasks.length);
   224      assert.ok(Exec.taskGroups[0].chevron.isDown);
   225    });
   226  
   227    test('navigating to a task adds its name to the route, chooses an allocation, and assigns a default command', async function(assert) {
   228      await Exec.visitJob({ job: this.job.id });
   229      await Exec.taskGroups[0].click();
   230      await Exec.taskGroups[0].tasks[0].click();
   231  
   232      let taskGroup = this.job.task_groups.models.sortBy('name')[0];
   233      let task = taskGroup.tasks.models.sortBy('name')[0];
   234  
   235      let taskStates = this.server.db.taskStates.where({
   236        name: task.name,
   237      });
   238      let allocationId = taskStates.find(ts => ts.allocationId).allocationId;
   239  
   240      await settled();
   241  
   242      assert.equal(currentURL(), `/exec/${this.job.id}/${taskGroup.name}/${task.name}`);
   243      assert.ok(Exec.taskGroups[0].tasks[0].isActive);
   244  
   245      assert.equal(
   246        window.execTerminal.buffer
   247          .getLine(2)
   248          .translateToString()
   249          .trim(),
   250        'Multiple instances of this task are running. The allocation below was selected by random draw.'
   251      );
   252  
   253      assert.equal(
   254        window.execTerminal.buffer
   255          .getLine(4)
   256          .translateToString()
   257          .trim(),
   258        'Customize your command, then hit ‘return’ to run.'
   259      );
   260  
   261      assert.equal(
   262        window.execTerminal.buffer
   263          .getLine(6)
   264          .translateToString()
   265          .trim(),
   266        `$ nomad alloc exec -i -t -task ${task.name} ${allocationId.split('-')[0]} /bin/bash`
   267      );
   268    });
   269  
   270    test('an allocation can be specified', async function(assert) {
   271      let taskGroup = this.job.task_groups.models.sortBy('name')[0];
   272      let task = taskGroup.tasks.models.sortBy('name')[0];
   273      let allocations = this.server.db.allocations.where({
   274        jobId: this.job.id,
   275        taskGroup: taskGroup.name,
   276      });
   277      let allocation = allocations[allocations.length - 1];
   278  
   279      this.server.db.taskStates.update({ name: task.name }, { name: 'spaced name!' });
   280  
   281      task.name = 'spaced name!';
   282      task.save();
   283  
   284      await Exec.visitTask({
   285        job: this.job.id,
   286        task_group: taskGroup.name,
   287        task_name: task.name,
   288        allocation: allocation.id.split('-')[0],
   289      });
   290  
   291      await settled();
   292  
   293      assert.equal(
   294        window.execTerminal.buffer
   295          .getLine(4)
   296          .translateToString()
   297          .trim(),
   298        `$ nomad alloc exec -i -t -task spaced\\ name\\! ${allocation.id.split('-')[0]} /bin/bash`
   299      );
   300    });
   301  
   302    test('running the command opens the socket for reading/writing and detects it closing', async function(assert) {
   303      let mockSocket = new MockSocket();
   304      let mockSockets = Service.extend({
   305        getTaskStateSocket(taskState, command) {
   306          assert.equal(taskState.name, task.name);
   307          assert.equal(taskState.allocation.id, allocation.id);
   308  
   309          assert.equal(command, '/bin/bash');
   310  
   311          assert.step('Socket built');
   312  
   313          return mockSocket;
   314        },
   315      });
   316  
   317      this.owner.register('service:sockets', mockSockets);
   318  
   319      let taskGroup = this.job.task_groups.models.sortBy('name')[0];
   320      let task = taskGroup.tasks.models.sortBy('name')[0];
   321      let allocations = this.server.db.allocations.where({
   322        jobId: this.job.id,
   323        taskGroup: taskGroup.name,
   324      });
   325      let allocation = allocations[allocations.length - 1];
   326  
   327      await Exec.visitTask({
   328        job: this.job.id,
   329        task_group: taskGroup.name,
   330        task_name: task.name,
   331        allocation: allocation.id.split('-')[0],
   332      });
   333  
   334      await settled();
   335  
   336      await Exec.terminal.pressEnter();
   337      await settled();
   338      mockSocket.onopen();
   339  
   340      assert.verifySteps(['Socket built']);
   341  
   342      mockSocket.onmessage({
   343        data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}',
   344      });
   345  
   346      await settled();
   347  
   348      assert.equal(
   349        window.execTerminal.buffer
   350          .getLine(5)
   351          .translateToString()
   352          .trim(),
   353        'sh-3.2 🥳$'
   354      );
   355  
   356      await Exec.terminal.pressEnter();
   357      await settled();
   358  
   359      assert.deepEqual(mockSocket.sent, [
   360        '{"version":1,"auth_token":""}',
   361        `{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`,
   362        '{"stdin":{"data":"DQ=="}}',
   363      ]);
   364  
   365      await mockSocket.onclose();
   366      await settled();
   367  
   368      assert.equal(
   369        window.execTerminal.buffer
   370          .getLine(6)
   371          .translateToString()
   372          .trim(),
   373        'The connection has closed.'
   374      );
   375    });
   376  
   377    test('the opening message includes the token if it exists', async function(assert) {
   378      const { secretId } = server.create('token');
   379      window.localStorage.nomadTokenSecret = secretId;
   380  
   381      let mockSocket = new MockSocket();
   382      let mockSockets = Service.extend({
   383        getTaskStateSocket() {
   384          return mockSocket;
   385        },
   386      });
   387  
   388      this.owner.register('service:sockets', mockSockets);
   389  
   390      let taskGroup = this.job.task_groups.models[0];
   391      let task = taskGroup.tasks.models[0];
   392      let allocations = this.server.db.allocations.where({
   393        jobId: this.job.id,
   394        taskGroup: taskGroup.name,
   395      });
   396      let allocation = allocations[allocations.length - 1];
   397  
   398      await Exec.visitTask({
   399        job: this.job.id,
   400        task_group: taskGroup.name,
   401        task_name: task.name,
   402        allocation: allocation.id.split('-')[0],
   403      });
   404  
   405      await Exec.terminal.pressEnter();
   406      await settled();
   407      mockSocket.onopen();
   408  
   409      await Exec.terminal.pressEnter();
   410      await settled();
   411  
   412      assert.equal(mockSocket.sent[0], `{"version":1,"auth_token":"${secretId}"}`);
   413    });
   414  
   415    test('only one socket is opened after switching between tasks', async function(assert) {
   416      let mockSockets = Service.extend({
   417        getTaskStateSocket() {
   418          assert.step('Socket built');
   419          return new MockSocket();
   420        },
   421      });
   422  
   423      this.owner.register('service:sockets', mockSockets);
   424  
   425      await Exec.visitJob({
   426        job: this.job.id,
   427      });
   428  
   429      await settled();
   430  
   431      await Exec.taskGroups[0].click();
   432      await Exec.taskGroups[0].tasks[0].click();
   433  
   434      await Exec.taskGroups[1].click();
   435      await Exec.taskGroups[1].tasks[0].click();
   436  
   437      await Exec.terminal.pressEnter();
   438  
   439      assert.verifySteps(['Socket built']);
   440    });
   441  
   442    test('the command can be customised', async function(assert) {
   443      let mockSockets = Service.extend({
   444        getTaskStateSocket(taskState, command) {
   445          assert.equal(command, '/sh');
   446          window.localStorage.getItem('nomadExecCommand', JSON.stringify('/sh'));
   447  
   448          assert.step('Socket built');
   449  
   450          return new MockSocket();
   451        },
   452      });
   453  
   454      this.owner.register('service:sockets', mockSockets);
   455  
   456      await Exec.visitJob({ job: this.job.id });
   457      await Exec.taskGroups[0].click();
   458      await Exec.taskGroups[0].tasks[0].click();
   459  
   460      let taskGroup = this.job.task_groups.models.sortBy('name')[0];
   461      let task = taskGroup.tasks.models.sortBy('name')[0];
   462      let allocation = this.server.db.allocations.findBy({
   463        jobId: this.job.id,
   464        taskGroup: taskGroup.name,
   465      });
   466  
   467      await settled();
   468  
   469      // Delete /bash
   470      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   471      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   472      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   473      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   474      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   475  
   476      // Delete /bin and try to go beyond
   477      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   478      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   479      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   480      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   481      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   482      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   483      await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
   484  
   485      await settled();
   486  
   487      assert.equal(
   488        window.execTerminal.buffer
   489          .getLine(6)
   490          .translateToString()
   491          .trim(),
   492        `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]}`
   493      );
   494  
   495      await window.execTerminal.simulateCommandDataEvent('/sh');
   496  
   497      await Exec.terminal.pressEnter();
   498      await settled();
   499  
   500      assert.verifySteps(['Socket built']);
   501    });
   502  
   503    test('a persisted customised command is recalled', async function(assert) {
   504      window.localStorage.setItem('nomadExecCommand', JSON.stringify('/bin/sh'));
   505  
   506      let taskGroup = this.job.task_groups.models[0];
   507      let task = taskGroup.tasks.models[0];
   508      let allocations = this.server.db.allocations.where({
   509        jobId: this.job.id,
   510        taskGroup: taskGroup.name,
   511      });
   512      let allocation = allocations[allocations.length - 1];
   513  
   514      await Exec.visitTask({
   515        job: this.job.id,
   516        task_group: taskGroup.name,
   517        task_name: task.name,
   518        allocation: allocation.id.split('-')[0],
   519      });
   520  
   521      await settled();
   522  
   523      assert.equal(
   524        window.execTerminal.buffer
   525          .getLine(4)
   526          .translateToString()
   527          .trim(),
   528        `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]} /bin/sh`
   529      );
   530    });
   531  
   532    skip('when a task state finishes submitting a command displays an error', async function(assert) {
   533      let taskGroup = this.job.task_groups.models.sortBy('name')[0];
   534      let task = taskGroup.tasks.models.sortBy('name')[0];
   535  
   536      await Exec.visitTask({
   537        job: this.job.id,
   538        task_group: taskGroup.name,
   539        task_name: task.name,
   540      });
   541  
   542      // Approximate allocation failure via polling
   543      this.owner
   544        .lookup('service:store')
   545        .peekAll('allocation')
   546        .forEach(allocation => allocation.set('clientStatus', 'failed'));
   547  
   548      await Exec.terminal.pressEnter();
   549      await settled();
   550  
   551      assert.equal(
   552        window.execTerminal.buffer
   553          .getLine(7)
   554          .translateToString()
   555          .trim(),
   556        `Failed to open a socket because task ${task.name} is not active.`
   557      );
   558    });
   559  });
   560  
   561  class MockSocket {
   562    constructor() {
   563      this.sent = [];
   564    }
   565  
   566    send(message) {
   567      this.sent.push(message);
   568    }
   569  }