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: Promote canary #4616

Merged
merged 3 commits into from
Aug 30, 2018
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
29 changes: 29 additions & 0 deletions ui/app/adapters/deployment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Watchable from './watchable';

export default Watchable.extend({
promote(deployment) {
const id = deployment.get('id');
const url = urlForAction(this.urlForFindRecord(id, 'deployment'), '/promote');
return this.ajax(url, 'POST', {
data: {
DeploymentId: id,
All: true,
},
});
},
});

// The deployment action API endpoints all end with the ID
// /deployment/:action/:deployment_id instead of /deployment/:deployment_id/:action
function urlForAction(url, extension = '') {
const [path, params] = url.split('?');
const pathParts = path.split('/');
const idPart = pathParts.pop();
let newUrl = `${pathParts.join('/')}${extension}/${idPart}`;

if (params) {
newUrl += `?${params}`;
}

return newUrl;
}
19 changes: 19 additions & 0 deletions ui/app/components/job-page/parts/latest-deployment.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import Component from '@ember/component';
import { task } from 'ember-concurrency';
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';

export default Component.extend({
job: null,
tagName: '',

handleError() {},

isShowingDeploymentDetails: false,

promote: task(function*() {
try {
yield this.get('job.latestDeployment.content').promote();
} catch (err) {
let message = messageFromAdapterError(err);
if (!message || message === 'Forbidden') {
message = 'Your ACL token does not grant permission to promote deployments.';
}
this.get('handleError')({
title: 'Could Not Promote Deployment',
description: message,
});
}
}),
});
6 changes: 6 additions & 0 deletions ui/app/models/deployment.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { alias, equal } from '@ember/object/computed';
import { computed } from '@ember/object';
import { assert } from '@ember/debug';
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { belongsTo, hasMany } from 'ember-data/relationships';
Expand Down Expand Up @@ -58,4 +59,9 @@ export default Model.extend({

return classMap[this.get('status')] || 'is-dark';
}),

promote() {
assert('A deployment needs to requirePromotion to be promoted', this.get('requiresPromotion'));
return this.store.adapterFor('deployment').promote(this);
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
{{job.latestDeployment.status}}
</span>
{{#if job.latestDeployment.requiresPromotion}}
<span class="tag bumper-left is-warning no-text-transform">Deployment is running but requires promotion</span>
<button
data-test-promote-canary
type="button"
class="button is-warning is-small pull-right {{if promote.isRunning "is-loading"}}"
disabled={{promote.isRunning}}
onclick={{perform promote}}>Promote Canary</button>
{{/if}}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion ui/app/templates/components/job-page/service.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

{{job-page/parts/placement-failures job=job}}

{{job-page/parts/latest-deployment job=job}}
{{job-page/parts/latest-deployment job=job handleError=(action "handleError")}}

{{job-page/parts/task-groups
job=job
Expand Down
3 changes: 3 additions & 0 deletions ui/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ export default function() {
});

this.get('/deployment/:id');
this.post('/deployment/promote/:id', function() {
return new Response(204, {}, '');
});

this.get('/job/:id/evaluations', function({ evaluations }, { params }) {
return this.serialize(evaluations.where({ jobId: params.id }));
Expand Down
2 changes: 2 additions & 0 deletions ui/mirage/factories/deployment-task-group-summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export default Factory.extend({
autoRevert: () => Math.random() > 0.5,
promoted: () => Math.random() > 0.5,

requiresPromotion: false,

requireProgressBy: () => faker.date.past(0.5 / 365, REF_TIME),

desiredTotal: faker.random.number({ min: 1, max: 10 }),
Expand Down
2 changes: 2 additions & 0 deletions ui/mirage/factories/deployment.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export default Factory.extend({
server.create('deployment-task-group-summary', {
deployment,
name: server.db.taskGroups.find(id).name,
desiredCanaries: 1,
promoted: false,
})
);

Expand Down
77 changes: 77 additions & 0 deletions ui/tests/integration/job-page/service-test.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { getOwner } from '@ember/application';
import { assign } from '@ember/polyfills';
import { test, moduleForComponent } from 'ember-qunit';
import { click, find } from 'ember-native-dom-helpers';
import wait from 'ember-test-helpers/wait';
import hbs from 'htmlbars-inline-precompile';
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
import { stopJob, expectStopError, expectDeleteRequest } from './helpers';
import Job from 'nomad-ui/tests/pages/jobs/detail';
import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';

moduleForComponent('job-page/service', 'Integration | Component | job-page/service', {
integration: true,
beforeEach() {
Job.setContext(this);
fragmentSerializerInitializer(getOwner(this));
window.localStorage.clear();
this.store = getOwner(this).lookup('service:store');
this.server = startMirage();
Expand Down Expand Up @@ -165,3 +168,77 @@ test('Recent allocations shows an empty message when the job has no allocations'
);
});
});

test('Active deployment can be promoted', function(assert) {
let job;
let deployment;

this.server.create('node');
const mirageJob = makeMirageJob(this.server, { activeDeployment: true });

this.store.findAll('job');

return wait()
.then(() => {
job = this.store.peekAll('job').findBy('plainId', mirageJob.id);
deployment = job.get('latestDeployment');

this.setProperties(commonProperties(job));
this.render(commonTemplate);

return wait();
})
.then(() => {
click('[data-test-promote-canary]');
return wait();
})
.then(() => {
const requests = this.server.pretender.handledRequests;
assert.ok(
requests
.filterBy('method', 'POST')
.findBy('url', `/v1/deployment/promote/${deployment.get('id')}`),
'A promote POST request was made'
);
});
});

test('When promoting the active deployment fails, an error is shown', function(assert) {
this.server.pretender.post('/v1/deployment/promote/:id', () => [403, {}, null]);

let job;

this.server.create('node');
const mirageJob = makeMirageJob(this.server, { activeDeployment: true });

this.store.findAll('job');

return wait()
.then(() => {
job = this.store.peekAll('job').findBy('plainId', mirageJob.id);

this.setProperties(commonProperties(job));
this.render(commonTemplate);

return wait();
})
.then(() => {
click('[data-test-promote-canary]');
return wait();
})
.then(() => {
assert.equal(
find('[data-test-job-error-title]').textContent,
'Could Not Promote Deployment',
'Appropriate error is shown'
);
assert.ok(
find('[data-test-job-error-body]').textContent.includes('ACL'),
'The error message mentions ACLs'
);

click('[data-test-job-error-close]');
assert.notOk(find('[data-test-job-error-title]'), 'Error message is dismissable');
return wait();
});
});