Merge pull request #8477 from hashicorp/b-ui/cross-region-alloc-lifecycle-buttons

UI: Always add the active region as a query param to API requests
This commit is contained in:
Michael Lange 2020-07-21 09:21:54 -07:00 committed by GitHub
commit ec4b96b5e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 381 additions and 168 deletions

View File

@ -21,6 +21,7 @@ BUG FIXES:
* ui: Fixed missing namespace query param after changing acl tokens [[GH-8413](https://github.com/hashicorp/nomad/issues/8413)]
* ui: Fixed exec to derive group and task when possible from allocation [[GH-8463](https://github.com/hashicorp/nomad/pull/8463)]
* ui: Fixed runtime error when clicking "Run Job" while a prefix filter is set [[GH-8412](https://github.com/hashicorp/nomad/issues/8412)]
* ui: Fixed the absence of the region query parameter on various actions, such as job stop, allocation restart, node drain. [[GH-8477](https://github.com/hashicorp/nomad/issues/8477)]
* vault: Fixed a bug where vault identity policies not considered in permissions check [[GH-7732](https://github.com/hashicorp/nomad/issues/7732)]
## 0.12.0 (July 9, 2020)

View File

@ -49,15 +49,18 @@ export default class ApplicationAdapter extends RESTAdapter {
});
}
ajaxOptions(url, type, options = {}) {
ajaxOptions(url, verb, options = {}) {
options.data || (options.data = {});
if (this.get('system.shouldIncludeRegion')) {
// Region should only ever be a query param. The default ajaxOptions
// behavior is to include data attributes in the requestBody for PUT
// and POST requests. This works around that.
const region = this.get('system.activeRegion');
if (region) {
options.data.region = region;
url = associateRegion(url, region);
}
}
return super.ajaxOptions(url, type, options);
return super.ajaxOptions(url, verb, options);
}
// In order to remove stale records from the store, findHasMany has to unload
@ -119,3 +122,7 @@ export default class ApplicationAdapter extends RESTAdapter {
return this.urlForFindRecord(...arguments);
}
}
function associateRegion(url, region) {
return url.indexOf('?') !== -1 ? `${url}&region=${region}` : `${url}?region=${region}`;
}

View File

@ -9,69 +9,108 @@ module('Unit | Adapter | Allocation', function(hooks) {
this.store = this.owner.lookup('service:store');
this.subject = () => this.store.adapterFor('allocation');
window.localStorage.clear();
this.server = startMirage();
this.server.create('namespace');
this.server.create('node');
this.server.create('job', { createAllocations: false });
this.server.create('allocation', { id: 'alloc-1' });
this.initialize = async (allocationId, { region } = {}) => {
if (region) window.localStorage.nomadActiveRegion = region;
this.server.create('namespace');
this.server.create('region', { id: 'region-1' });
this.server.create('region', { id: 'region-2' });
this.server.create('node');
this.server.create('job', { createAllocations: false });
this.server.create('allocation', { id: 'alloc-1' });
this.system = this.owner.lookup('service:system');
await this.system.get('namespaces');
this.system.get('shouldIncludeRegion');
await this.system.get('defaultRegion');
const allocation = await this.store.findRecord('allocation', allocationId);
this.server.pretender.handledRequests.length = 0;
return allocation;
};
});
hooks.afterEach(function() {
this.server.shutdown();
});
test('`stop` makes the correct API call', async function(assert) {
const { pretender } = this.server;
const allocationId = 'alloc-1';
const testCases = [
{
variation: '',
id: 'alloc-1',
task: 'task-name',
region: null,
path: 'some/path',
ls: `GET /v1/client/fs/ls/alloc-1?path=${encodeURIComponent('some/path')}`,
stat: `GET /v1/client/fs/stat/alloc-1?path=${encodeURIComponent('some/path')}`,
stop: 'POST /v1/allocation/alloc-1/stop',
restart: 'PUT /v1/client/allocation/alloc-1/restart',
},
{
variation: 'with non-default region',
id: 'alloc-1',
task: 'task-name',
region: 'region-2',
path: 'some/path',
ls: `GET /v1/client/fs/ls/alloc-1?path=${encodeURIComponent('some/path')}&region=region-2`,
stat: `GET /v1/client/fs/stat/alloc-1?path=${encodeURIComponent(
'some/path'
)}&region=region-2`,
stop: 'POST /v1/allocation/alloc-1/stop?region=region-2',
restart: 'PUT /v1/client/allocation/alloc-1/restart?region=region-2',
},
];
const allocation = await this.store.findRecord('allocation', allocationId);
pretender.handledRequests.length = 0;
testCases.forEach(testCase => {
test(`ls makes the correct API call ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
const allocation = await this.initialize(testCase.id, { region: testCase.region });
await this.subject().stop(allocation);
const req = pretender.handledRequests[0];
assert.equal(
`${req.method} ${req.url}`,
`POST /v1/allocation/${allocationId}/stop`,
`POST /v1/allocation/${allocationId}/stop`
);
});
await this.subject().ls(allocation, testCase.path);
const req = pretender.handledRequests[0];
assert.equal(`${req.method} ${req.url}`, testCase.ls);
});
test('`restart` makes the correct API call', async function(assert) {
const { pretender } = this.server;
const allocationId = 'alloc-1';
test(`stat makes the correct API call ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
const allocation = await this.initialize(testCase.id, { region: testCase.region });
const allocation = await this.store.findRecord('allocation', allocationId);
pretender.handledRequests.length = 0;
await this.subject().stat(allocation, testCase.path);
const req = pretender.handledRequests[0];
assert.equal(`${req.method} ${req.url}`, testCase.stat);
});
await this.subject().restart(allocation);
const req = pretender.handledRequests[0];
assert.equal(
`${req.method} ${req.url}`,
`PUT /v1/client/allocation/${allocationId}/restart`,
`PUT /v1/client/allocation/${allocationId}/restart`
);
});
test(`stop makes the correct API call ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
const allocation = await this.initialize(testCase.id, { region: testCase.region });
test('`restart` takes an optional task name and makes the correct API call', async function(assert) {
const { pretender } = this.server;
const allocationId = 'alloc-1';
const taskName = 'task-name';
await this.subject().stop(allocation);
const req = pretender.handledRequests[0];
assert.equal(`${req.method} ${req.url}`, testCase.stop);
});
const allocation = await this.store.findRecord('allocation', allocationId);
pretender.handledRequests.length = 0;
test(`restart makes the correct API call ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
const allocation = await this.initialize(testCase.id, { region: testCase.region });
await this.subject().restart(allocation, taskName);
const req = pretender.handledRequests[0];
assert.equal(
`${req.method} ${req.url}`,
`PUT /v1/client/allocation/${allocationId}/restart`,
`PUT /v1/client/allocation/${allocationId}/restart`
);
assert.deepEqual(
JSON.parse(req.requestBody),
{ TaskName: taskName },
'Request body is correct'
);
await this.subject().restart(allocation);
const req = pretender.handledRequests[0];
assert.equal(`${req.method} ${req.url}`, testCase.restart);
});
test(`restart with optional task name makes the correct API call ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
const allocation = await this.initialize(testCase.id, { region: testCase.region });
await this.subject().restart(allocation, testCase.task);
const req = pretender.handledRequests[0];
assert.equal(`${req.method} ${req.url}`, testCase.restart);
assert.deepEqual(JSON.parse(req.requestBody), { TaskName: testCase.task });
});
});
});

View File

@ -0,0 +1,68 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
module('Unit | Adapter | Deployment', function(hooks) {
setupTest(hooks);
hooks.beforeEach(async function() {
this.store = this.owner.lookup('service:store');
this.system = this.owner.lookup('service:system');
this.subject = () => this.store.adapterFor('deployment');
window.localStorage.clear();
this.server = startMirage();
this.initialize = async ({ region } = {}) => {
if (region) window.localStorage.nomadActiveRegion = region;
this.server.create('region', { id: 'region-1' });
this.server.create('region', { id: 'region-2' });
this.server.create('node');
const job = this.server.create('job', { createAllocations: false });
const deploymentRecord = server.schema.deployments.where({ jobId: job.id }).models[0];
this.system.get('shouldIncludeRegion');
await this.system.get('defaultRegion');
const deployment = await this.store.findRecord('deployment', deploymentRecord.id);
this.server.pretender.handledRequests.length = 0;
return deployment;
};
});
hooks.afterEach(function() {
this.server.shutdown();
});
const testCases = [
{
variation: '',
region: null,
promote: id => `POST /v1/deployment/promote/${id}`,
},
{
variation: 'with non-default region',
region: 'region-2',
promote: id => `POST /v1/deployment/promote/${id}?region=region-2`,
},
];
testCases.forEach(testCase => {
test(`promote makes the correct API call ${testCase.variation}`, async function(assert) {
const deployment = await this.initialize({ region: testCase.region });
await this.subject().promote(deployment);
const request = this.server.pretender.handledRequests[0];
assert.equal(`${request.method} ${request.url}`, testCase.promote(deployment.id));
assert.deepEqual(JSON.parse(request.requestBody), {
DeploymentId: deployment.id,
All: true,
});
});
});
});

View File

@ -18,7 +18,10 @@ module('Unit | Adapter | Job', function(hooks) {
this.server = startMirage();
this.initializeUI = async () => {
this.initializeUI = async ({ region, namespace } = {}) => {
if (namespace) window.localStorage.nomadActiveNamespace = namespace;
if (region) window.localStorage.nomadActiveRegion = region;
this.server.create('namespace');
this.server.create('namespace', { id: 'some-namespace' });
this.server.create('node');
@ -41,6 +44,17 @@ module('Unit | Adapter | Job', function(hooks) {
// namespaces request everywhere.
this.server.pretender.handledRequests.length = 0;
};
this.initializeWithJob = async (props = {}) => {
await this.initializeUI(props);
const job = await this.store.findRecord(
'job',
JSON.stringify(['job-1', props.namespace || 'default'])
);
this.server.pretender.handledRequests.length = 0;
return job;
};
});
hooks.afterEach(function() {
@ -66,9 +80,7 @@ module('Unit | Adapter | Job', function(hooks) {
});
test('When a namespace is set in localStorage but a job in the default namespace is requested, the namespace query param is not present', async function(assert) {
window.localStorage.nomadActiveNamespace = 'some-namespace';
await this.initializeUI();
await this.initializeUI({ namespace: 'some-namespace' });
const { pretender } = this.server;
const jobName = 'job-1';
@ -86,9 +98,7 @@ module('Unit | Adapter | Job', function(hooks) {
});
test('When a namespace is in localStorage and the requested job is in the default namespace, the namespace query param is left out', async function(assert) {
window.localStorage.nomadActiveNamespace = 'red-herring';
await this.initializeUI();
await this.initializeUI({ namespace: 'red-herring' });
const { pretender } = this.server;
const jobName = 'job-1';
@ -399,9 +409,8 @@ module('Unit | Adapter | Job', function(hooks) {
test('when there is a region set, requests are made with the region query param', async function(assert) {
const region = 'region-2';
window.localStorage.nomadActiveRegion = region;
await this.initializeUI();
await this.initializeUI({ region });
const { pretender } = this.server;
const jobName = 'job-1';
@ -421,9 +430,7 @@ module('Unit | Adapter | Job', function(hooks) {
});
test('when the region is set to the default region, requests are made without the region query param', async function(assert) {
window.localStorage.nomadActiveRegion = 'region-1';
await this.initializeUI();
await this.initializeUI({ region: 'region-1' });
const { pretender } = this.server;
const jobName = 'job-1';
@ -441,6 +448,102 @@ module('Unit | Adapter | Job', function(hooks) {
'No requests include the region query param'
);
});
test('fetchRawDefinition requests include the activeRegion', async function(assert) {
const region = 'region-2';
const job = await this.initializeWithJob({ region });
await this.subject().fetchRawDefinition(job);
const request = this.server.pretender.handledRequests[0];
assert.equal(request.url, `/v1/job/${job.plainId}?region=${region}`);
assert.equal(request.method, 'GET');
});
test('forcePeriodic requests include the activeRegion', async function(assert) {
const region = 'region-2';
const job = await this.initializeWithJob({ region });
job.set('periodic', true);
await this.subject().forcePeriodic(job);
const request = this.server.pretender.handledRequests[0];
assert.equal(request.url, `/v1/job/${job.plainId}/periodic/force?region=${region}`);
assert.equal(request.method, 'POST');
});
test('stop requests include the activeRegion', async function(assert) {
const region = 'region-2';
const job = await this.initializeWithJob({ region });
await this.subject().stop(job);
const request = this.server.pretender.handledRequests[0];
assert.equal(request.url, `/v1/job/${job.plainId}?region=${region}`);
assert.equal(request.method, 'DELETE');
});
test('parse requests include the activeRegion', async function(assert) {
const region = 'region-2';
await this.initializeUI({ region });
await this.subject().parse('job "name-goes-here" {');
const request = this.server.pretender.handledRequests[0];
assert.equal(request.url, `/v1/jobs/parse?region=${region}`);
assert.equal(request.method, 'POST');
assert.deepEqual(JSON.parse(request.requestBody), {
JobHCL: 'job "name-goes-here" {',
Canonicalize: true,
});
});
test('plan requests include the activeRegion', async function(assert) {
const region = 'region-2';
const job = await this.initializeWithJob({ region });
job.set('_newDefinitionJSON', {});
await this.subject().plan(job);
const request = this.server.pretender.handledRequests[0];
assert.equal(request.url, `/v1/job/${job.plainId}/plan?region=${region}`);
assert.equal(request.method, 'POST');
});
test('run requests include the activeRegion', async function(assert) {
const region = 'region-2';
const job = await this.initializeWithJob({ region });
job.set('_newDefinitionJSON', {});
await this.subject().run(job);
const request = this.server.pretender.handledRequests[0];
assert.equal(request.url, `/v1/jobs?region=${region}`);
assert.equal(request.method, 'POST');
});
test('update requests include the activeRegion', async function(assert) {
const region = 'region-2';
const job = await this.initializeWithJob({ region });
job.set('_newDefinitionJSON', {});
await this.subject().update(job);
const request = this.server.pretender.handledRequests[0];
assert.equal(request.url, `/v1/job/${job.plainId}?region=${region}`);
assert.equal(request.method, 'POST');
});
test('scale requests include the activeRegion', async function(assert) {
const region = 'region-2';
const job = await this.initializeWithJob({ region });
await this.subject().scale(job, 'group-1', 5, 'Reason: a test');
const request = this.server.pretender.handledRequests[0];
assert.equal(request.url, `/v1/job/${job.plainId}/scale?region=${region}`);
assert.equal(request.method, 'POST');
});
});
function makeMockModel(id, options) {

View File

@ -11,7 +11,13 @@ module('Unit | Adapter | Node', function(hooks) {
this.store = this.owner.lookup('service:store');
this.subject = () => this.store.adapterFor('node');
window.localStorage.clear();
this.server = startMirage();
this.server.create('region', { id: 'region-1' });
this.server.create('region', { id: 'region-2' });
this.server.create('node', { id: 'node-1' });
this.server.create('node', { id: 'node-2' });
this.server.create('job', { id: 'job-1', createAllocations: false });
@ -86,157 +92,146 @@ module('Unit | Adapter | Node', function(hooks) {
);
});
test('setEligible makes the correct POST request to /:node_id/eligibility', async function(assert) {
const { pretender } = this.server;
const node = await run(() => this.store.findRecord('node', 'node-1'));
const testCases = [
{
variation: '',
id: 'node-1',
region: null,
eligibility: 'POST /v1/node/node-1/eligibility',
drain: 'POST /v1/node/node-1/drain',
},
{
variation: 'with non-default region',
id: 'node-1',
region: 'region-2',
eligibility: 'POST /v1/node/node-1/eligibility?region=region-2',
drain: 'POST /v1/node/node-1/drain?region=region-2',
},
];
await this.subject().setEligible(node);
testCases.forEach(testCase => {
test(`setEligible makes the correct POST request to /:node_id/eligibility ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
if (testCase.region) window.localStorage.nomadActiveRegion = testCase.region;
const request = pretender.handledRequests.lastObject;
assert.equal(
request.url,
`/v1/node/${node.id}/eligibility`,
'Request was made to /:node_id/eligibility'
);
assert.equal(request.method, 'POST', 'Request was made with the POST method');
assert.deepEqual(
JSON.parse(request.requestBody),
{
const node = await run(() => this.store.findRecord('node', testCase.id));
await this.subject().setEligible(node);
const request = pretender.handledRequests.lastObject;
assert.equal(`${request.method} ${request.url}`, testCase.eligibility);
assert.deepEqual(JSON.parse(request.requestBody), {
NodeID: node.id,
Eligibility: 'eligible',
},
'POST request is made with the correct body arguments'
);
});
});
});
test('setIneligible makes the correct POST request to /:node_id/eligibility', async function(assert) {
const { pretender } = this.server;
const node = await run(() => this.store.findRecord('node', 'node-1'));
test(`setIneligible makes the correct POST request to /:node_id/eligibility ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
if (testCase.region) window.localStorage.nomadActiveRegion = testCase.region;
await this.subject().setIneligible(node);
const node = await run(() => this.store.findRecord('node', testCase.id));
await this.subject().setIneligible(node);
const request = pretender.handledRequests.lastObject;
assert.equal(
request.url,
`/v1/node/${node.id}/eligibility`,
'Request was made to /:node_id/eligibility'
);
assert.equal(request.method, 'POST', 'Request was made with the POST method');
assert.deepEqual(
JSON.parse(request.requestBody),
{
const request = pretender.handledRequests.lastObject;
assert.equal(`${request.method} ${request.url}`, testCase.eligibility);
assert.deepEqual(JSON.parse(request.requestBody), {
NodeID: node.id,
Eligibility: 'ineligible',
},
'POST request is made with the correct body arguments'
);
});
});
});
test('drain makes the correct POST request to /:node_id/drain with appropriate defaults', async function(assert) {
const { pretender } = this.server;
const node = await run(() => this.store.findRecord('node', 'node-1'));
test(`drain makes the correct POST request to /:node_id/drain with appropriate defaults ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
if (testCase.region) window.localStorage.nomadActiveRegion = testCase.region;
await this.subject().drain(node);
const node = await run(() => this.store.findRecord('node', testCase.id));
await this.subject().drain(node);
const request = pretender.handledRequests.lastObject;
assert.equal(request.url, `/v1/node/${node.id}/drain`, 'Request was made to /:node_id/drain');
assert.equal(request.method, 'POST', 'Request was made with the POST method');
assert.deepEqual(
JSON.parse(request.requestBody),
{
const request = pretender.handledRequests.lastObject;
assert.equal(`${request.method} ${request.url}`, testCase.drain);
assert.deepEqual(JSON.parse(request.requestBody), {
NodeID: node.id,
DrainSpec: {
Deadline: 0,
IgnoreSystemJobs: true,
},
},
'POST request is made with the default body arguments'
);
});
});
});
test('drain makes the correct POST request to /:node_id/drain with the provided drain spec', async function(assert) {
const { pretender } = this.server;
const node = await run(() => this.store.findRecord('node', 'node-1'));
test(`drain makes the correct POST request to /:node_id/drain with the provided drain spec ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
if (testCase.region) window.localStorage.nomadActiveRegion = testCase.region;
const spec = { Deadline: 123456789, IgnoreSystemJobs: false };
await this.subject().drain(node, spec);
const node = await run(() => this.store.findRecord('node', testCase.id));
const request = pretender.handledRequests.lastObject;
assert.deepEqual(
JSON.parse(request.requestBody),
{
const spec = { Deadline: 123456789, IgnoreSystemJobs: false };
await this.subject().drain(node, spec);
const request = pretender.handledRequests.lastObject;
assert.equal(`${request.method} ${request.url}`, testCase.drain);
assert.deepEqual(JSON.parse(request.requestBody), {
NodeID: node.id,
DrainSpec: {
Deadline: spec.Deadline,
IgnoreSystemJobs: spec.IgnoreSystemJobs,
},
},
'POST request is made with the drain spec as body arguments'
);
});
});
});
test('forceDrain makes the correct POST request to /:node_id/drain with appropriate defaults', async function(assert) {
const { pretender } = this.server;
const node = await run(() => this.store.findRecord('node', 'node-1'));
test(`forceDrain makes the correct POST request to /:node_id/drain with appropriate defaults ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
if (testCase.region) window.localStorage.nomadActiveRegion = testCase.region;
await this.subject().forceDrain(node);
const node = await run(() => this.store.findRecord('node', testCase.id));
const request = pretender.handledRequests.lastObject;
assert.equal(request.url, `/v1/node/${node.id}/drain`, 'Request was made to /:node_id/drain');
assert.equal(request.method, 'POST', 'Request was made with the POST method');
assert.deepEqual(
JSON.parse(request.requestBody),
{
await this.subject().forceDrain(node);
const request = pretender.handledRequests.lastObject;
assert.equal(`${request.method} ${request.url}`, testCase.drain);
assert.deepEqual(JSON.parse(request.requestBody), {
NodeID: node.id,
DrainSpec: {
Deadline: -1,
IgnoreSystemJobs: true,
},
},
'POST request is made with the default body arguments'
);
});
});
});
test('forceDrain makes the correct POST request to /:node_id/drain with the provided drain spec', async function(assert) {
const { pretender } = this.server;
const node = await run(() => this.store.findRecord('node', 'node-1'));
test(`forceDrain makes the correct POST request to /:node_id/drain with the provided drain spec ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
if (testCase.region) window.localStorage.nomadActiveRegion = testCase.region;
const spec = { Deadline: 123456789, IgnoreSystemJobs: false };
await this.subject().forceDrain(node, spec);
const node = await run(() => this.store.findRecord('node', testCase.id));
const request = pretender.handledRequests.lastObject;
assert.equal(request.url, `/v1/node/${node.id}/drain`, 'Request was made to /:node_id/drain');
assert.equal(request.method, 'POST', 'Request was made with the POST method');
assert.deepEqual(
JSON.parse(request.requestBody),
{
const spec = { Deadline: 123456789, IgnoreSystemJobs: false };
await this.subject().forceDrain(node, spec);
const request = pretender.handledRequests.lastObject;
assert.equal(`${request.method} ${request.url}`, testCase.drain);
assert.deepEqual(JSON.parse(request.requestBody), {
NodeID: node.id,
DrainSpec: {
Deadline: -1,
IgnoreSystemJobs: spec.IgnoreSystemJobs,
},
},
'POST request is made with the drain spec, except deadline is not overridden'
);
});
});
});
test('cancelDrain makes the correct POST request to /:node_id/drain', async function(assert) {
const { pretender } = this.server;
const node = await run(() => this.store.findRecord('node', 'node-1'));
test(`cancelDrain makes the correct POST request to /:node_id/drain ${testCase.variation}`, async function(assert) {
const { pretender } = this.server;
if (testCase.region) window.localStorage.nomadActiveRegion = testCase.region;
await this.subject().cancelDrain(node);
const node = await run(() => this.store.findRecord('node', testCase.id));
const request = pretender.handledRequests.lastObject;
assert.equal(request.url, `/v1/node/${node.id}/drain`, 'Request was made to /:node_id/drain');
assert.equal(request.method, 'POST', 'Request was made with the POST method');
assert.deepEqual(
JSON.parse(request.requestBody),
{
await this.subject().cancelDrain(node);
const request = pretender.handledRequests.lastObject;
assert.equal(`${request.method} ${request.url}`, testCase.drain);
assert.deepEqual(JSON.parse(request.requestBody), {
NodeID: node.id,
DrainSpec: null,
},
'POST request is made with a null drain spec'
);
});
});
});
});