github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/acceptance/exec-test.js (about)

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