Treat namespace and job name as a composite primary key

This commit is contained in:
Michael Lange 2017-10-23 10:22:58 -07:00
parent 8eeacebe67
commit afcfba0910
7 changed files with 105 additions and 21 deletions

View file

@ -1,7 +1,7 @@
import Ember from 'ember'; import Ember from 'ember';
import ApplicationAdapter from './application'; import ApplicationAdapter from './application';
const { RSVP, inject } = Ember; const { RSVP, inject, assign } = Ember;
export default ApplicationAdapter.extend({ export default ApplicationAdapter.extend({
system: inject.service(), system: inject.service(),
@ -9,12 +9,10 @@ export default ApplicationAdapter.extend({
shouldReloadAll: () => true, shouldReloadAll: () => true,
buildQuery() { buildQuery() {
const namespace = this.get('system.activeNamespace'); const namespace = this.get('system.activeNamespace.id');
if (namespace) { if (namespace && namespace !== 'default') {
return { return { namespace };
namespace: namespace.get('name'),
};
} }
}, },
@ -22,7 +20,7 @@ export default ApplicationAdapter.extend({
const namespace = this.get('system.activeNamespace'); const namespace = this.get('system.activeNamespace');
return this._super(...arguments).then(data => { return this._super(...arguments).then(data => {
data.forEach(job => { data.forEach(job => {
job.Namespace = namespace && namespace.get('id'); job.Namespace = namespace ? namespace.get('id') : 'default';
}); });
return data; return data;
}); });
@ -31,11 +29,21 @@ export default ApplicationAdapter.extend({
findRecord(store, { modelName }, id, snapshot) { findRecord(store, { modelName }, id, snapshot) {
// To make a findRecord response reflect the findMany response, the JobSummary // To make a findRecord response reflect the findMany response, the JobSummary
// from /summary needs to be stitched into the response. // from /summary needs to be stitched into the response.
// URL is the form of /job/:name?namespace=:namespace with arbitrary additional query params
const [name, namespace] = JSON.parse(id);
const namespaceQuery = namespace && namespace !== 'default' ? { namespace } : {};
return RSVP.hash({ return RSVP.hash({
job: this._super(...arguments), job: this.ajax(this.buildURL(modelName, name, snapshot, 'findRecord'), 'GET', {
summary: this.ajax(`${this.buildURL(modelName, id, snapshot, 'findRecord')}/summary`, 'GET', { data: assign(this.buildQuery() || {}, namespaceQuery),
data: this.buildQuery(),
}), }),
summary: this.ajax(
`${this.buildURL(modelName, name, snapshot, 'findRecord')}/summary`,
'GET',
{
data: assign(this.buildQuery() || {}, namespaceQuery),
}
),
}).then(({ job, summary }) => { }).then(({ job, summary }) => {
job.JobSummary = summary; job.JobSummary = summary;
return job; return job;
@ -52,7 +60,9 @@ export default ApplicationAdapter.extend({
}, },
fetchRawDefinition(job) { fetchRawDefinition(job) {
const url = this.buildURL('job', job.get('id'), job, 'findRecord'); const [name, namespace] = JSON.parse(job.get('id'));
return this.ajax(url, 'GET', { data: this.buildQuery() }); const namespaceQuery = namespace && namespace !== 'default' ? { namespace } : {};
const url = this.buildURL('job', name, job, 'findRecord');
return this.ajax(url, 'GET', { data: assign(this.buildQuery() || {}, namespaceQuery) });
}, },
}); });

View file

@ -10,6 +10,7 @@ const { computed } = Ember;
export default Model.extend({ export default Model.extend({
region: attr('string'), region: attr('string'),
name: attr('string'), name: attr('string'),
plainId: attr('string'),
type: attr('string'), type: attr('string'),
priority: attr('number'), priority: attr('number'),
allAtOnce: attr('boolean'), allAtOnce: attr('boolean'),

View file

@ -1,9 +1,11 @@
import Ember from 'ember'; import Ember from 'ember';
import ApplicationSerializer from './application'; import ApplicationSerializer from './application';
const { get } = Ember; const { get, inject } = Ember;
export default ApplicationSerializer.extend({ export default ApplicationSerializer.extend({
system: inject.service(),
attrs: { attrs: {
taskGroupName: 'TaskGroup', taskGroupName: 'TaskGroup',
states: 'TaskStates', states: 'TaskStates',
@ -22,6 +24,14 @@ export default ApplicationSerializer.extend({
hash.JobVersion = hash.JobVersion != null ? hash.JobVersion : get(hash, 'Job.Version'); hash.JobVersion = hash.JobVersion != null ? hash.JobVersion : get(hash, 'Job.Version');
hash.PlainJobId = hash.JobID;
hash.Namespace =
hash.Namespace ||
get(hash, 'Job.Namespace') ||
this.get('system.activeNamespace.id') ||
'default';
hash.JobID = JSON.stringify([hash.JobID, hash.Namespace]);
// TEMPORARY: https://github.com/emberjs/data/issues/5209 // TEMPORARY: https://github.com/emberjs/data/issues/5209
hash.OriginalJobId = hash.JobID; hash.OriginalJobId = hash.JobID;

View file

@ -14,6 +14,14 @@ export default ApplicationSerializer.extend({
return assign({ Name: key }, deploymentStats); return assign({ Name: key }, deploymentStats);
}); });
hash.PlainJobId = hash.JobID;
hash.Namespace =
hash.Namespace ||
get(hash, 'Job.Namespace') ||
this.get('system.activeNamespace.id') ||
'default';
hash.JobID = JSON.stringify([hash.JobID, hash.Namespace]);
return this._super(typeHash, hash); return this._super(typeHash, hash);
}, },

View file

@ -12,9 +12,13 @@ export default ApplicationSerializer.extend({
normalize(typeHash, hash) { normalize(typeHash, hash) {
hash.NamespaceID = hash.Namespace; hash.NamespaceID = hash.Namespace;
// ID is a composite of both the job ID and the namespace the job is in
hash.PlainId = hash.ID;
hash.ID = JSON.stringify([hash.ID, hash.NamespaceID || 'default']);
// Transform the map-based JobSummary object into an array-based // Transform the map-based JobSummary object into an array-based
// JobSummary fragment list // JobSummary fragment list
hash.TaskGroupSummaries = Object.keys(get(hash, 'JobSummary.Summary')).map(key => { hash.TaskGroupSummaries = Object.keys(get(hash, 'JobSummary.Summary') || {}).map(key => {
const allocStats = get(hash, `JobSummary.Summary.${key}`); const allocStats = get(hash, `JobSummary.Summary.${key}`);
const summary = { Name: key }; const summary = { Name: key };
@ -40,9 +44,10 @@ export default ApplicationSerializer.extend({
const namespace = const namespace =
!hash.NamespaceID || hash.NamespaceID === 'default' ? undefined : hash.NamespaceID; !hash.NamespaceID || hash.NamespaceID === 'default' ? undefined : hash.NamespaceID;
const { modelName } = modelClass; const { modelName } = modelClass;
const jobURL = this.store const jobURL = this.store
.adapterFor(modelName) .adapterFor(modelName)
.buildURL(modelName, this.extractId(modelClass, hash), hash, 'findRecord'); .buildURL(modelName, hash.PlainId, hash, 'findRecord');
return assign(this._super(...arguments), { return assign(this._super(...arguments), {
allocations: { allocations: {

View file

@ -10,6 +10,7 @@ moduleFor('adapter:job', 'Unit | Adapter | Job', {
this.server = startMirage(); this.server = startMirage();
this.server.create('node'); this.server.create('node');
this.server.create('job', { id: 'job-1' }); this.server.create('job', { id: 'job-1' });
this.server.create('job', { id: 'job-2', namespaceId: 'some-namespace' });
}, },
afterEach() { afterEach() {
this.server.shutdown(); this.server.shutdown();
@ -18,22 +19,43 @@ moduleFor('adapter:job', 'Unit | Adapter | Job', {
test('The job summary is stitched into the job request', function(assert) { test('The job summary is stitched into the job request', function(assert) {
const { pretender } = this.server; const { pretender } = this.server;
const jobId = 'job-1'; const jobName = 'job-1';
const jobNamespace = 'default';
const jobId = JSON.stringify([jobName, jobNamespace]);
this.subject().findRecord(null, { modelName: 'job' }, jobId); this.subject().findRecord(null, { modelName: 'job' }, jobId);
assert.deepEqual( assert.deepEqual(
pretender.handledRequests.mapBy('url'), pretender.handledRequests.mapBy('url'),
['/v1/namespaces', `/v1/job/${jobId}`, `/v1/job/${jobId}/summary`], ['/v1/namespaces', `/v1/job/${jobName}`, `/v1/job/${jobName}/summary`],
'The three requests made are /namespaces, /job/:id, and /job/:id/summary' 'The three requests made are /namespaces, /job/:id, and /job/:id/summary'
); );
}); });
test('When the job has a namespace other than default, it is in the URL', function(assert) {
const { pretender } = this.server;
const jobName = 'job-2';
const jobNamespace = 'some-namespace';
const jobId = JSON.stringify([jobName, jobNamespace]);
this.subject().findRecord(null, { modelName: 'job' }, jobId);
assert.deepEqual(
pretender.handledRequests.mapBy('url'),
[
'/v1/namespaces',
`/v1/job/${jobName}?namespace=${jobNamespace}`,
`/v1/job/${jobName}/summary?namespace=${jobNamespace}`,
],
'The three requests made are /namespaces, /job/:id?namespace=:namespace, and /job/:id/summary?namespace=:namespace'
);
});
test('When there is no token set in the token service, no x-nomad-token header is set', function( test('When there is no token set in the token service, no x-nomad-token header is set', function(
assert assert
) { ) {
const { pretender } = this.server; const { pretender } = this.server;
const jobId = 'job-1'; const jobId = JSON.stringify(['job-1', 'default']);
this.subject().findRecord(null, { modelName: 'job' }, jobId); this.subject().findRecord(null, { modelName: 'job' }, jobId);
@ -47,7 +69,7 @@ test('When a token is set in the token service, then x-nomad-token header is set
assert assert
) { ) {
const { pretender } = this.server; const { pretender } = this.server;
const jobId = 'job-1'; const jobId = JSON.stringify(['job-1', 'default']);
const secret = 'here is the secret'; const secret = 'here is the secret';
this.subject().set('token.secret', secret); this.subject().set('token.secret', secret);

View file

@ -51,10 +51,11 @@ test('The JobSummary object is transformed from a map to a list', function(asser
JobModifyIndex: 7, JobModifyIndex: 7,
}; };
const normalized = this.subject().normalize(JobModel, original); const { data } = this.subject().normalize(JobModel, original);
assert.deepEqual(normalized.data.attributes, { assert.deepEqual(data.attributes, {
name: 'example', name: 'example',
plainId: 'example',
type: 'service', type: 'service',
priority: 50, priority: 50,
periodic: false, periodic: false,
@ -116,6 +117,7 @@ test('The children stats are lifted out of the JobSummary object', function(asse
assert.deepEqual(normalized.data.attributes, { assert.deepEqual(normalized.data.attributes, {
name: 'example', name: 'example',
plainId: 'example',
type: 'service', type: 'service',
priority: 50, priority: 50,
periodic: false, periodic: false,
@ -130,3 +132,29 @@ test('The children stats are lifted out of the JobSummary object', function(asse
modifyIndex: 9, modifyIndex: 9,
}); });
}); });
test('`default` is used as the namespace in the job ID when there is no namespace in the payload', function(
assert
) {
const original = {
ID: 'example',
Name: 'example',
};
const { data } = this.subject().normalize(JobModel, original);
assert.equal(data.id, JSON.stringify([data.attributes.name, 'default']));
});
test('The ID of the record is a composite of both the name and the namespace', function(assert) {
const original = {
ID: 'example',
Name: 'example',
Namespace: 'special-namespace',
};
const { data } = this.subject().normalize(JobModel, original);
assert.equal(
data.id,
JSON.stringify([data.attributes.name, data.relationships.namespace.data.id])
);
});