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