Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: set the job namespace when redirecting after the job is dispatched #11141

Merged
merged 3 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/11141.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: Fixed an issue when dispatching jobs from a non-default namespace
```
4 changes: 3 additions & 1 deletion ui/app/components/job-dispatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ export default class JobDispatch extends Component {
const dispatch = yield this.args.job.dispatch(paramValues, this.payload);

// Navigate to the newly created instance.
this.router.transitionTo('jobs.job', dispatch.DispatchedJobID);
this.router.transitionTo('jobs.job', dispatch.DispatchedJobID, {
queryParams: { namespace: this.args.job.get('namespace.name') },
});
} catch (err) {
const error = messageFromAdapterError(err) || 'Could not dispatch job';
this.errors.pushObject(error);
Expand Down
2 changes: 1 addition & 1 deletion ui/app/templates/components/job-page/parts/title.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<h1 class="title with-flex">
<div>
<div data-test-job-name>
{{or this.title this.job.name}}
<span class="bumper-left tag {{this.job.statusClass}}" data-test-job-status>{{this.job.status}}</span>
{{yield}}
Expand Down
326 changes: 173 additions & 153 deletions ui/tests/acceptance/job-dispatch-test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable ember/no-test-module-for */
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
Expand All @@ -9,197 +10,216 @@ import { currentURL } from '@ember/test-helpers';

const REQUIRED_INDICATOR = '*';

let job, namespace, managementToken, clientToken;
moduleForJobDispatch('Acceptance | job dispatch', () => {
server.createList('namespace', 2);
const namespace = server.db.namespaces[0];

module('Acceptance | job dispatch', function(hooks) {
setupApplicationTest(hooks);
setupCodeMirror(hooks);
setupMirage(hooks);

hooks.beforeEach(function() {
// Required for placing allocations (a result of dispatching jobs)
server.create('node');
server.createList('namespace', 2);

namespace = server.db.namespaces[0];
job = server.create('job', 'parameterized', {
status: 'running',
namespaceId: namespace.name,
});

managementToken = server.create('token');
clientToken = server.create('token');

window.localStorage.nomadTokenSecret = managementToken.secretId;
return server.create('job', 'parameterized', {
status: 'running',
namespaceId: namespace.name,
});
});

test('it passes an accessibility audit', async function(assert) {
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await a11yAudit(assert);
});
moduleForJobDispatch('Acceptance | job dispatch (with namespace)', () => {
server.createList('namespace', 2);
const namespace = server.db.namespaces[1];

test('the dispatch button is displayed with management token', async function(assert) {
await JobDetail.visit({ id: job.id, namespace: namespace.name });
assert.notOk(JobDetail.dispatchButton.isDisabled);
return server.create('job', 'parameterized', {
status: 'running',
namespaceId: namespace.name,
});
});

test('the dispatch button is displayed when allowed', async function(assert) {
window.localStorage.nomadTokenSecret = clientToken.secretId;

const policy = server.create('policy', {
id: 'dispatch',
name: 'dispatch',
rulesJSON: {
Namespaces: [
{
Name: namespace.name,
Capabilities: ['list-jobs', 'dispatch-job'],
},
],
},
});

clientToken.policyIds = [policy.id];
clientToken.save();

await JobDetail.visit({ id: job.id, namespace: namespace.name });
assert.notOk(JobDetail.dispatchButton.isDisabled);
function moduleForJobDispatch(title, jobFactory) {
let job, namespace, managementToken, clientToken;

// Reset clientToken policies.
clientToken.policyIds = [];
clientToken.save();
});
module(title, function(hooks) {
setupApplicationTest(hooks);
setupCodeMirror(hooks);
setupMirage(hooks);

test('the dispatch button is disabled when not allowed', async function(assert) {
window.localStorage.nomadTokenSecret = clientToken.secretId;
hooks.beforeEach(function() {
// Required for placing allocations (a result of dispatching jobs)
server.create('node');

await JobDetail.visit({ id: job.id, namespace: namespace.name });
assert.ok(JobDetail.dispatchButton.isDisabled);
});
job = jobFactory();
namespace = server.db.namespaces.find(job.namespaceId);

test('all meta fields are displayed', async function(assert) {
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
assert.equal(
JobDispatch.metaFields.length,
job.parameterizedJob.MetaOptional.length + job.parameterizedJob.MetaRequired.length
);
});
managementToken = server.create('token');
clientToken = server.create('token');

test('required meta fields are properly indicated', async function(assert) {
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
window.localStorage.nomadTokenSecret = managementToken.secretId;
});

JobDispatch.metaFields.forEach(f => {
const hasIndicator = f.label.includes(REQUIRED_INDICATOR);
const isRequired = job.parameterizedJob.MetaRequired.includes(f.field.id);
test('it passes an accessibility audit', async function(assert) {
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await a11yAudit(assert);
});

if (isRequired) {
assert.ok(hasIndicator, `${f.label} contains required indicator.`);
} else {
assert.notOk(hasIndicator, `${f.label} doesn't contain required indicator.`);
}
test('the dispatch button is displayed with management token', async function(assert) {
await JobDetail.visit({ id: job.id, namespace: namespace.name });
assert.notOk(JobDetail.dispatchButton.isDisabled);
});
});

test('job without meta fields', async function(assert) {
const jobWithoutMeta = server.create('job', 'parameterized', {
status: 'running',
namespaceId: namespace.name,
parameterizedJob: {
MetaRequired: null,
MetaOptional: null,
},
test('the dispatch button is displayed when allowed', async function(assert) {
window.localStorage.nomadTokenSecret = clientToken.secretId;

const policy = server.create('policy', {
id: 'dispatch',
name: 'dispatch',
rulesJSON: {
Namespaces: [
{
Name: namespace.name,
Capabilities: ['list-jobs', 'dispatch-job'],
},
],
},
});

clientToken.policyIds = [policy.id];
clientToken.save();

await JobDetail.visit({ id: job.id, namespace: namespace.name });
assert.notOk(JobDetail.dispatchButton.isDisabled);

// Reset clientToken policies.
clientToken.policyIds = [];
clientToken.save();
});

await JobDispatch.visit({ id: jobWithoutMeta.id, namespace: namespace.name });
assert.ok(JobDispatch.dispatchButton.isPresent);
});
test('the dispatch button is disabled when not allowed', async function(assert) {
window.localStorage.nomadTokenSecret = clientToken.secretId;

test('payload text area is hidden when forbidden', async function(assert) {
job.parameterizedJob.Payload = 'forbidden';
job.save();
await JobDetail.visit({ id: job.id, namespace: namespace.name });
assert.ok(JobDetail.dispatchButton.isDisabled);
});

await JobDispatch.visit({ id: job.id, namespace: namespace.name });
test('all meta fields are displayed', async function(assert) {
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
assert.equal(
JobDispatch.metaFields.length,
job.parameterizedJob.MetaOptional.length + job.parameterizedJob.MetaRequired.length
);
});

assert.ok(JobDispatch.payload.emptyMessage.isPresent);
assert.notOk(JobDispatch.payload.editor.isPresent);
});
test('required meta fields are properly indicated', async function(assert) {
await JobDispatch.visit({ id: job.id, namespace: namespace.name });

test('payload is indicated as required', async function(assert) {
const jobPayloadRequired = server.create('job', 'parameterized', {
status: 'running',
namespaceId: namespace.name,
parameterizedJob: {
Payload: 'required',
},
JobDispatch.metaFields.forEach(f => {
const hasIndicator = f.label.includes(REQUIRED_INDICATOR);
const isRequired = job.parameterizedJob.MetaRequired.includes(f.field.id);

if (isRequired) {
assert.ok(hasIndicator, `${f.label} contains required indicator.`);
} else {
assert.notOk(hasIndicator, `${f.label} doesn't contain required indicator.`);
}
});
});
const jobPayloadOptional = server.create('job', 'parameterized', {
status: 'running',
namespaceId: namespace.name,
parameterizedJob: {
Payload: 'optional',
},

test('job without meta fields', async function(assert) {
const jobWithoutMeta = server.create('job', 'parameterized', {
status: 'running',
namespaceId: namespace.name,
parameterizedJob: {
MetaRequired: null,
MetaOptional: null,
},
});

await JobDispatch.visit({ id: jobWithoutMeta.id, namespace: namespace.name });
assert.ok(JobDispatch.dispatchButton.isPresent);
});

await JobDispatch.visit({ id: jobPayloadRequired.id, namespace: namespace.name });
test('payload text area is hidden when forbidden', async function(assert) {
job.parameterizedJob.Payload = 'forbidden';
job.save();

let payloadTitle = JobDispatch.payload.title;
assert.ok(
payloadTitle.includes(REQUIRED_INDICATOR),
`${payloadTitle} contains required indicator.`
);
await JobDispatch.visit({ id: job.id, namespace: namespace.name });

await JobDispatch.visit({ id: jobPayloadOptional.id, namespace: namespace.name });
assert.ok(JobDispatch.payload.emptyMessage.isPresent);
assert.notOk(JobDispatch.payload.editor.isPresent);
});

payloadTitle = JobDispatch.payload.title;
assert.notOk(
payloadTitle.includes(REQUIRED_INDICATOR),
`${payloadTitle} doesn't contain required indicator.`
);
});
test('payload is indicated as required', async function(assert) {
const jobPayloadRequired = server.create('job', 'parameterized', {
status: 'running',
namespaceId: namespace.name,
parameterizedJob: {
Payload: 'required',
},
});
const jobPayloadOptional = server.create('job', 'parameterized', {
status: 'running',
namespaceId: namespace.name,
parameterizedJob: {
Payload: 'optional',
},
});

await JobDispatch.visit({ id: jobPayloadRequired.id, namespace: namespace.name });

let payloadTitle = JobDispatch.payload.title;
assert.ok(
payloadTitle.includes(REQUIRED_INDICATOR),
`${payloadTitle} contains required indicator.`
);

await JobDispatch.visit({ id: jobPayloadOptional.id, namespace: namespace.name });

payloadTitle = JobDispatch.payload.title;
assert.notOk(
payloadTitle.includes(REQUIRED_INDICATOR),
`${payloadTitle} doesn't contain required indicator.`
);
});

test('dispatch a job', async function(assert) {
function countDispatchChildren() {
return server.db.jobs.where(j => j.id.startsWith(`${job.id}/`)).length;
}
test('dispatch a job', async function(assert) {
function countDispatchChildren() {
return server.db.jobs.where(j => j.id.startsWith(`${job.id}/`)).length;
}

await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.visit({ id: job.id, namespace: namespace.name });

// Fill form.
JobDispatch.metaFields.map(f => f.field.input('meta value'));
JobDispatch.payload.editor.fillIn('payload');
// Fill form.
JobDispatch.metaFields.map(f => f.field.input('meta value'));
JobDispatch.payload.editor.fillIn('payload');

const childrenCountBefore = countDispatchChildren();
await JobDispatch.dispatchButton.click();
const childrenCountAfter = countDispatchChildren();
const childrenCountBefore = countDispatchChildren();
await JobDispatch.dispatchButton.click();
const childrenCountAfter = countDispatchChildren();

assert.equal(childrenCountAfter, childrenCountBefore + 1);
assert.ok(currentURL().startsWith(`/jobs/${encodeURIComponent(`${job.id}/`)}`));
});
assert.equal(childrenCountAfter, childrenCountBefore + 1);
assert.ok(currentURL().startsWith(`/jobs/${encodeURIComponent(`${job.id}/`)}`));
assert.ok(JobDetail.jobName);
});

test('fail when required meta field is empty', async function(assert) {
// Make sure we have a required meta param.
job.parameterizedJob.MetaRequired = ['required'];
job.parameterizedJob.Payload = 'forbidden';
job.save();
test('fail when required meta field is empty', async function(assert) {
// Make sure we have a required meta param.
job.parameterizedJob.MetaRequired = ['required'];
job.parameterizedJob.Payload = 'forbidden';
job.save();

await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.visit({ id: job.id, namespace: namespace.name });

// Fill only optional meta params.
JobDispatch.optionalMetaFields.map(f => f.field.input('meta value'));
// Fill only optional meta params.
JobDispatch.optionalMetaFields.map(f => f.field.input('meta value'));

await JobDispatch.dispatchButton.click();
await JobDispatch.dispatchButton.click();

assert.ok(JobDispatch.hasError, 'Dispatch error message is shown');
});
assert.ok(JobDispatch.hasError, 'Dispatch error message is shown');
});

test('fail when required payload is empty', async function(assert) {
job.parameterizedJob.MetaRequired = [];
job.parameterizedJob.Payload = 'required';
job.save();
test('fail when required payload is empty', async function(assert) {
job.parameterizedJob.MetaRequired = [];
job.parameterizedJob.Payload = 'required';
job.save();

await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.dispatchButton.click();
await JobDispatch.visit({ id: job.id, namespace: namespace.name });
await JobDispatch.dispatchButton.click();

assert.ok(JobDispatch.hasError, 'Dispatch error message is shown');
assert.ok(JobDispatch.hasError, 'Dispatch error message is shown');
});
});
});
}
Loading