Skip to content

Commit

Permalink
Job Services: fixtures and acceptance tests (#14319)
Browse files Browse the repository at this point in the history
* Added to subnav and basic table implemented

* Existing services become service fragments, and services tab aggregated beneath job route

* Index page within jobs/job/services

* Watchable services

* Lintfixes

* Links to clients and individual services set up

* Child service route

* Keyboard shortcuts on service page

* Model that shows consul services as well, plus level and provider cols

* lintfix

* Level as query param

* Watch job for service name changes too

* Group level service fixtures established

* Progress at task level and job-linked services

* Task and group services on update

* Fixture side-effect cleanup

* Basic acceptance tests for job services

* Testmodel cleanup

* Disabled mirage logging

* New cluster type specifically for services

* Without explicit job-model binding

* Trying to isolate a tostring error

* Account for new tab in keyboardnav

* More test isolation attempts

* Remove skipped tests and link task to parent group by id

ui: add service health viz to table (#14369)

* ui: add service-status-bar

* test: service-status-bar

* refact: update component api for new data struct

* ui: format service health struct

* ui:  add service health viz to table

* temp: add placeholder to remind conditional watcher

* test: write tests for transformation algorithm

* refact: update transformation algo

* ui: conditionally long poll checks endpoint

* refact: add conditional logic for nomad provider

refact: update service-fragment model to include owner info

ui: differentiate between task and group-level in derived state comp

test: add test to document behavior

refact: update tests for api change

refact: update integration test for API change

chore: remove unsused vars

chore: elvis operator to protect mirage

refact: create refId instead of internalModel

refact: update algo

refact: update conditional template logic

refact: update test for api change:

chore: cant use if and not in hbs conditional
  • Loading branch information
philrenaud committed Aug 31, 2022
1 parent b1a73ef commit 01a8c94
Show file tree
Hide file tree
Showing 26 changed files with 831 additions and 57 deletions.
13 changes: 8 additions & 5 deletions ui/app/components/service-status-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ export default class ServiceStatusBar extends DistributionBar {
layoutName = 'components/distribution-bar';

services = null;
name = null;

'data-test-service-status-bar' = true;

@computed('services.@each.status')
@computed('services.{}', 'name')
get data() {
if (!this.services) {
const service = this.services && this.services.get(this.name);

if (!service) {
return [];
}

const pending = this.services.filterBy('status', 'pending').length;
const failing = this.services.filterBy('status', 'failing').length;
const success = this.services.filterBy('status', 'success').length;
const pending = service.pending || 0;
const failing = service.failure || 0;
const success = service.success || 0;

const [grey, red, green] = ['queued', 'failed', 'complete'];

Expand Down
34 changes: 34 additions & 0 deletions ui/app/controllers/allocations/allocation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,40 @@ export default class IndexController extends Controller.extend(Sortable) {

@union('taskServices', 'groupServices') services;

@computed('model.healthChecks.{}')
get serviceHealthStatuses() {
if (!this.model.healthChecks) return null;

let result = new Map();
Object.values(this.model.healthChecks)?.forEach((service) => {
const isTask = !!service.Task;
const groupName = service.Group.split('.')[1].split('[')[0];
const currentServiceStatus = service.Status;

const currentServiceName = isTask
? service.Task.concat(`-${service.Service}`)
: groupName.concat(`-${service.Service}`);
const serviceStatuses = result.get(currentServiceName);
if (serviceStatuses) {
if (serviceStatuses[currentServiceStatus]) {
result.set(currentServiceName, {
...serviceStatuses,
[currentServiceStatus]: serviceStatuses[currentServiceStatus]++,
});
} else {
result.set(currentServiceName, {
...serviceStatuses,
[currentServiceStatus]: 1,
});
}
} else {
result.set(currentServiceName, { [currentServiceStatus]: 1 });
}
});

return result;
}

onDismiss() {
this.set('error', null);
}
Expand Down
6 changes: 6 additions & 0 deletions ui/app/models/service-fragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ export default class Service extends Fragment {
@attr('string') onUpdate;
@attr('string') provider;
@fragment('consul-connect') connect;
@attr() groupName;
@attr() taskName;

get refID() {
return `${this.groupName || this.taskName}-${this.name}`;
}
}
19 changes: 15 additions & 4 deletions ui/app/routes/allocations/allocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,21 @@ export default class AllocationRoute extends Route.extend(WithWatchers) {
startWatchers(controller, model) {
if (model) {
controller.set('watcher', this.watch.perform(model));
controller.set(
'watchHealthChecks',
this.watchHealthChecks.perform(model, 'getServiceHealth')
);

// Conditionally Long Poll /checks endpoint if alloc has nomad services
const doesAllocHaveServices =
!!model.taskGroup?.services?.filterBy('provider', 'nomad').length ||
!!model.states
?.mapBy('task')
?.map((t) => t && t.get('services'))[0]
?.filterBy('provider', 'nomad').length;

if (doesAllocHaveServices) {
controller.set(
'watchHealthChecks',
this.watchHealthChecks.perform(model, 'getServiceHealth')
);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion ui/app/serializers/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ApplicationSerializer from './application';
@classic
export default class ServiceSerializer extends ApplicationSerializer {
normalize(typeHash, hash) {
hash.AllocationID = hash.AllocID; // TODO: keyForRelationship maybe?
hash.AllocationID = hash.AllocID;
hash.JobID = JSON.stringify([hash.JobID, hash.Namespace]);
return super.normalize(typeHash, hash);
}
Expand Down
6 changes: 6 additions & 0 deletions ui/app/serializers/task-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export default class TaskGroup extends ApplicationSerializer {
mapToArray = ['Volumes'];

normalize(typeHash, hash) {
if (hash.Services) {
hash.Services.forEach((service) => {
service.GroupName = hash.Name;
});
}

// Provide EphemeralDisk to each task
hash.Tasks.forEach((task) => {
task.EphemeralDisk = copy(hash.EphemeralDisk);
Expand Down
14 changes: 14 additions & 0 deletions ui/app/templates/allocations/allocation/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@
<td>
Upstreams
</td>
<td>
Health Check Status
</td>
</t.head>
<t.body as |row|>
<tr data-test-service>
Expand All @@ -315,6 +318,17 @@
{{upstream.destinationName}}:{{upstream.localBindPort}}
{{/each}}
</td>
<td data-test-service-health>
{{#if (eq row.model.provider "nomad")}}
<div class="inline-chart">
{{#if (is-empty row.model.taskName)}}
<ServiceStatusBar @isNarrow={{true}} @services={{this.serviceHealthStatuses}} @name={{concat row.model.groupName "-" row.model.name}} />
{{else}}
<ServiceStatusBar @isNarrow={{true}} @services={{this.serviceHealthStatuses}} @name={{concat row.model.taskName "-" row.model.name}} />
{{/if}}
</div>
{{/if}}
</td>
</tr>
</t.body>
</ListTable>
Expand Down
12 changes: 10 additions & 2 deletions ui/app/templates/components/job-service-row.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
<tr {{on "click" (fn this.gotoService @service)}} class={{if (eq @service.provider "nomad") "is-interactive"}} data-test-service={{@service.id}}>
<tr
data-test-service-row
data-test-service-name={{@service.name}}
data-test-num-allocs={{@service.instances.length}}
data-test-service-provider={{@service.provider}}
data-test-service-level={{@service.level}}
{{on "click" (fn this.gotoService @service)}}
class={{if (eq @service.provider "nomad") "is-interactive"}}
>
<td>
{{#if (eq @service.provider "nomad")}}
<FlightIcon @name="nomad-color" />
Expand All @@ -22,7 +30,7 @@
{{@service.level}}
</td>
<td>
<LinkTo @route="clients.client" @model={{@service.instances.0.node.id}}>{{@service.instances.0.node.name}}</LinkTo>
<LinkTo @route="clients.client" @model={{@service.instances.0.node.id}}>{{@service.instances.0.node.shortId}}</LinkTo>
</td>
<td>
{{#each @service.tags as |tag|}}
Expand Down
2 changes: 1 addition & 1 deletion ui/app/templates/jobs/job/services/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
as |t|
>
<t.head>
<t.sort-by @prop="provider"></t.sort-by>
<t.sort-by @prop="provider">Provider</t.sort-by>
<t.sort-by @prop="name">Name</t.sort-by>
<t.sort-by @prop="provider">Level</t.sort-by>
<t.sort-by @prop="client">Client</t.sort-by>
Expand Down
4 changes: 1 addition & 3 deletions ui/app/templates/jobs/job/services/service.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@

<ListTable
@source={{this.model.instances}}
{{!-- @sortProperty={{this.sortProperty}}
@sortDescending={{this.sortDescending}} --}}
as |t|
>
<t.head>
<t.sort-by @prop="allocation">Allocation</t.sort-by>
<t.sort-by @prop="address">IP Address &amp; Port</t.sort-by>
</t.head>
<t.body as |row|>
<tr>
<tr data-test-service-row>
<td
{{keyboard-shortcut
enumerated=true
Expand Down
4 changes: 4 additions & 0 deletions ui/mirage/factories/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ export default Factory.extend({
// When true, task groups will have services
withGroupServices: false,

// When true, tasks will have services
withTaskServices: false,

// When true, dynamic application sizing recommendations will be made
createRecommendations: false,

Expand All @@ -211,6 +214,7 @@ export default Factory.extend({
createAllocations: job.createAllocations,
withRescheduling: job.withRescheduling,
withServices: job.withGroupServices,
withTaskServices: job.withTaskServices,
createRecommendations: job.createRecommendations,
shallow: job.shallow,
};
Expand Down
36 changes: 36 additions & 0 deletions ui/mirage/factories/service-fragment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Factory } from 'ember-cli-mirage';
import faker from 'nomad-ui/mirage/faker';
import { provide } from '../utils';
import { dasherize } from '@ember/string';
import { pickOne } from '../utils';

const ON_UPDATE = ['default', 'ignore', 'ignore_warnings'];

export default Factory.extend({
name: (id) => `${dasherize(faker.hacker.noun())}-${id}-service`,
portLabel: () => dasherize(faker.hacker.noun()),
onUpdate: faker.helpers.randomize(ON_UPDATE),
provider: () => pickOne(['nomad', 'consul']),
tags: () => {
if (!faker.random.boolean()) {
return provide(
faker.random.number({ min: 0, max: 2 }),
faker.hacker.noun.bind(faker.hacker.noun)
);
} else {
return null;
}
},
Connect: {
SidecarService: {
Proxy: {
Upstreams: [
{
DestinationName: dasherize(faker.hacker.noun()),
LocalBindPort: faker.random.number({ min: 5000, max: 60000 }),
},
],
},
},
},
});
59 changes: 43 additions & 16 deletions ui/mirage/factories/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import { Factory } from 'ember-cli-mirage';
import faker from 'nomad-ui/mirage/faker';
import { provide } from '../utils';
import { dasherize } from '@ember/string';

const ON_UPDATE = ['default', 'ignore', 'ignore_warnings'];
import { DATACENTERS } from '../common';
import { pickOne } from '../utils';

export default Factory.extend({
name: (id) => `${dasherize(faker.hacker.noun())}-${id}-service`,
portLabel: () => dasherize(faker.hacker.noun()),
onUpdate: faker.helpers.randomize(ON_UPDATE),
id: () => faker.random.uuid(),
address: () => faker.internet.ip(),
createIndex: () => faker.random.number(),
modifyIndex: () => faker.random.number(),
name: () => faker.random.uuid(),
serviceName: (id) => `${dasherize(faker.hacker.noun())}-${id}-service`,
datacenter: faker.helpers.randomize(DATACENTERS),
port: faker.random.number({ min: 5000, max: 60000 }),
tags: () => {
if (!faker.random.boolean()) {
return provide(
Expand All @@ -19,16 +24,38 @@ export default Factory.extend({
return null;
}
},
Connect: {
SidecarService: {
Proxy: {
Upstreams: [
{
DestinationName: dasherize(faker.hacker.noun()),
LocalBindPort: faker.random.number({ min: 5000, max: 60000 }),
},
],
},
},

afterCreate(service, server) {
if (!service.namespace) {
const namespace = pickOne(server.db.jobs)?.namespace || 'default';
service.update({
namespace,
});
}

if (!service.node) {
const node = pickOne(server.db.nodes);
service.update({
nodeId: node.id,
});
}

if (server.db.jobs.findBy({ id: 'service-haver' })) {
if (!service.jobId) {
service.update({
jobId: 'service-haver',
});
}
if (!service.allocId) {
const servicedAlloc = pickOne(
server.db.allocations.filter((a) => a.jobId === 'service-haver') || []
);
if (servicedAlloc) {
service.update({
allocId: servicedAlloc.id,
});
}
}
}
},
});
43 changes: 36 additions & 7 deletions ui/mirage/factories/task-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export default Factory.extend({
// Directive used to control whether the task group should have services.
withServices: false,

// Whether the tasks themselves should have services.
withTaskServices: false,

// Directive used to control whether dynamic application sizing recommendations
// should be created.
createRecommendations: false,
Expand Down Expand Up @@ -88,8 +91,9 @@ export default Factory.extend({
maybeResources.originalResources = generateResources(resources[idx]);
}
return server.create('task', {
taskGroup: group,
taskGroupID: group.id,
...maybeResources,
withServices: group.withTaskServices,
volumeMounts: mounts.map((mount) => ({
Volume: mount,
Destination: `/${faker.internet.userName()}/${faker.internet.domainWord()}/${faker.internet.color()}`,
Expand Down Expand Up @@ -132,13 +136,38 @@ export default Factory.extend({
}

if (group.withServices) {
Array(faker.random.number({ min: 1, max: 3 }))
.fill(null)
.forEach(() => {
server.create('service', {
taskGroup: group,
});
const services = server.createList('service-fragment', 5, {
taskGroupId: group.id,
taskGroup: group,
provider: 'nomad',
});

services.push(
server.create('service-fragment', {
taskGroupId: group.id,
taskGroup: group,
provider: 'consul',
})
);

services.forEach((fragment) => {
server.create('service', {
serviceName: fragment.name,
id: `${faker.internet.domainWord()}-group-${fragment.name}`,
});
server.create('service', {
serviceName: fragment.name,
id: `${faker.internet.domainWord()}-group-${fragment.name}`,
});
server.create('service', {
serviceName: fragment.name,
id: `${faker.internet.domainWord()}-group-${fragment.name}`,
});
});

group.update({
services,
});
}
},
});
Expand Down
Loading

0 comments on commit 01a8c94

Please sign in to comment.