mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
[ui] Service Discovery: Allocation Service fly-out (#14389)
* Bones of a new flyout section * Basic sidebar behaviour and style edits * Concept of a refID for service fragments to disambiguate task and group * A11y audit etc * Moves health check aggregation to serviceFragment model and retains history * Has to be a getter * flyout populated * Sidebar styling * Sidebar table and details added * Mirage fixture * Active status and table styles * Unit test mock updated * Acceptance tests for alloc services table and flyout * Chart styles closer to mock * Without a paused test * Consul and Nomad icons in services table * Alloc services test updates in light of new column changes * without using an inherited scenario
This commit is contained in:
130
ui/app/components/allocation-service-sidebar.hbs
Normal file
130
ui/app/components/allocation-service-sidebar.hbs
Normal file
@@ -0,0 +1,130 @@
|
||||
<div
|
||||
class="sidebar has-subnav service-sidebar {{if this.isSideBarOpen "open"}}"
|
||||
{{on-click-outside
|
||||
@fns.closeSidebar
|
||||
capture=true
|
||||
}}
|
||||
>
|
||||
{{#if @service}}
|
||||
{{keyboard-commands this.keyCommands}}
|
||||
<header class="detail-header">
|
||||
<h1 class="title">
|
||||
{{@service.name}}
|
||||
<span class="aggregate-status">
|
||||
{{#if (eq this.aggregateStatus 'Unhealthy')}}
|
||||
<FlightIcon @name="x-square-fill" @color="#c84034" />
|
||||
Unhealthy
|
||||
{{else}}
|
||||
<FlightIcon @name="check-square-fill" @color="#25ba81" />
|
||||
Healthy
|
||||
{{/if}}
|
||||
</span>
|
||||
</h1>
|
||||
<button
|
||||
data-test-close-service-sidebar
|
||||
class="button is-borderless"
|
||||
type="button"
|
||||
{{on "click" @fns.closeSidebar}}
|
||||
>
|
||||
{{x-icon "cancel"}}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="boxed-section is-small">
|
||||
<div
|
||||
class="boxed-section-body inline-definitions"
|
||||
>
|
||||
<span class="label">
|
||||
Service Details
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<span class="pair">
|
||||
<span class="term">
|
||||
Allocation
|
||||
</span>
|
||||
<LinkTo
|
||||
@route="allocations.allocation"
|
||||
@model={{@allocation}}
|
||||
@query={{hash service=""}}
|
||||
>
|
||||
{{@allocation.shortId}}
|
||||
</LinkTo>
|
||||
</span>
|
||||
<span class="pair">
|
||||
<span class="term">
|
||||
IP Address & Port
|
||||
</span>
|
||||
<a
|
||||
href="http://{{this.address}}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{this.address}}
|
||||
</a>
|
||||
</span>
|
||||
{{#if @service.tags.length}}
|
||||
<span class="pair">
|
||||
<span class="term">
|
||||
Tags
|
||||
</span>
|
||||
{{join ", " @service.tags}}
|
||||
</span>
|
||||
{{/if}}
|
||||
<span class="pair">
|
||||
<span class="term">
|
||||
Client
|
||||
</span>
|
||||
<LinkTo
|
||||
@route="clients.client"
|
||||
@model={{@allocation.node}}
|
||||
>
|
||||
{{@allocation.node.shortId}}
|
||||
</LinkTo>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if @service.mostRecentChecks.length}}
|
||||
<ListTable class="health-checks" @source={{@service.mostRecentChecks}} as |t|>
|
||||
<t.head>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Status
|
||||
</th>
|
||||
<td>
|
||||
Output
|
||||
</td>
|
||||
</t.head>
|
||||
<t.body as |row|>
|
||||
<tr data-service-health={{row.model.Status}}>
|
||||
<td class="name">
|
||||
<span title={{row.model.Check}}>{{row.model.Check}}</span>
|
||||
</td>
|
||||
<td class="status">
|
||||
<span>
|
||||
{{#if (eq row.model.Status "success")}}
|
||||
<FlightIcon @name="check-square-fill" @color="#25ba81" />
|
||||
Healthy
|
||||
{{else if (eq row.model.Status "failure")}}
|
||||
<FlightIcon @name="x-square-fill" @color="#c84034" />
|
||||
Unhealthy
|
||||
{{else if (eq row.model.Status "pending")}}
|
||||
Pending
|
||||
{{/if}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="service-output">
|
||||
<code>
|
||||
{{row.model.Output}}
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
</t.body>
|
||||
</ListTable>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
41
ui/app/components/allocation-service-sidebar.js
Normal file
41
ui/app/components/allocation-service-sidebar.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class AllocationServiceSidebarComponent extends Component {
|
||||
@service store;
|
||||
|
||||
get isSideBarOpen() {
|
||||
return !!this.args.service;
|
||||
}
|
||||
keyCommands = [
|
||||
{
|
||||
label: 'Close Evaluations Sidebar',
|
||||
pattern: ['Escape'],
|
||||
action: () => this.args.fns.closeSidebar(),
|
||||
},
|
||||
];
|
||||
|
||||
get service() {
|
||||
return this.store.query('service-fragment', { refID: this.args.serviceID });
|
||||
}
|
||||
|
||||
get address() {
|
||||
const port = this.args.allocation?.allocatedResources?.ports?.findBy(
|
||||
'label',
|
||||
this.args.service.portLabel
|
||||
);
|
||||
if (port) {
|
||||
return `${port.hostIp}:${port.value}`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get aggregateStatus() {
|
||||
return this.args.service?.mostRecentChecks?.any(
|
||||
(check) => check.Status === 'failure'
|
||||
)
|
||||
? 'Unhealthy'
|
||||
: 'Healthy';
|
||||
}
|
||||
}
|
||||
@@ -8,22 +8,19 @@ import classic from 'ember-classic-decorator';
|
||||
export default class ServiceStatusBar extends DistributionBar {
|
||||
layoutName = 'components/distribution-bar';
|
||||
|
||||
services = null;
|
||||
name = null;
|
||||
status = null;
|
||||
|
||||
'data-test-service-status-bar' = true;
|
||||
|
||||
@computed('services.{}', 'name')
|
||||
@computed('status.{failure,pending,success}')
|
||||
get data() {
|
||||
const service = this.services && this.services.get(this.name);
|
||||
|
||||
if (!service) {
|
||||
if (!this.status) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pending = service.pending || 0;
|
||||
const failing = service.failure || 0;
|
||||
const success = service.success || 0;
|
||||
const pending = this.status.pending || 0;
|
||||
const failing = this.status.failure || 0;
|
||||
const success = this.status.success || 0;
|
||||
|
||||
const [grey, red, green] = ['queued', 'failed', 'complete'];
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { watchRecord } from 'nomad-ui/utils/properties/watch';
|
||||
import messageForError from 'nomad-ui/utils/message-from-adapter-error';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import { union } from '@ember/object/computed';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
@classic
|
||||
export default class IndexController extends Controller.extend(Sortable) {
|
||||
@@ -25,6 +26,9 @@ export default class IndexController extends Controller.extend(Sortable) {
|
||||
{
|
||||
sortDescending: 'desc',
|
||||
},
|
||||
{
|
||||
activeServiceID: 'service',
|
||||
},
|
||||
];
|
||||
|
||||
sortProperty = 'name';
|
||||
@@ -55,7 +59,7 @@ export default class IndexController extends Controller.extend(Sortable) {
|
||||
@computed('tasks.@each.services')
|
||||
get taskServices() {
|
||||
return this.get('tasks')
|
||||
.map((t) => ((t && t.get('services')) || []).toArray())
|
||||
.map((t) => ((t && t.services) || []).toArray())
|
||||
.flat()
|
||||
.compact();
|
||||
}
|
||||
@@ -67,38 +71,33 @@ 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 });
|
||||
@computed('model.healthChecks.{}', 'services')
|
||||
get servicesWithHealthChecks() {
|
||||
return this.services.map((service) => {
|
||||
if (this.model.healthChecks) {
|
||||
const healthChecks = Object.values(this.model.healthChecks)?.filter(
|
||||
(check) => {
|
||||
const refPrefix =
|
||||
check.Task || check.Group.split('.')[1].split('[')[0];
|
||||
const currentServiceName = `${refPrefix}-${check.Service}`;
|
||||
return currentServiceName === service.refID;
|
||||
}
|
||||
);
|
||||
// Only append those healthchecks whose timestamps are not already found in service.healthChecks
|
||||
healthChecks.forEach((check) => {
|
||||
if (
|
||||
!service.healthChecks.find(
|
||||
(sc) =>
|
||||
sc.Check === check.Check && sc.Timestamp === check.Timestamp
|
||||
)
|
||||
) {
|
||||
service.healthChecks.pushObject(check);
|
||||
service.healthChecks = [...service.healthChecks.slice(-10)];
|
||||
}
|
||||
});
|
||||
}
|
||||
return service;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
onDismiss() {
|
||||
@@ -165,4 +164,31 @@ export default class IndexController extends Controller.extend(Sortable) {
|
||||
taskClick(allocation, task, event) {
|
||||
lazyClick([() => this.send('gotoTask', allocation, task), event]);
|
||||
}
|
||||
|
||||
//#region Services
|
||||
|
||||
@tracked activeServiceID = null;
|
||||
|
||||
@action handleServiceClick(service) {
|
||||
this.set('activeServiceID', service.refID);
|
||||
}
|
||||
|
||||
@computed('activeServiceID', 'services')
|
||||
get activeService() {
|
||||
return this.services.findBy('refID', this.activeServiceID);
|
||||
}
|
||||
|
||||
@action closeSidebar() {
|
||||
this.set('activeServiceID', null);
|
||||
}
|
||||
|
||||
keyCommands = [
|
||||
{
|
||||
label: 'Close Evaluations Sidebar',
|
||||
pattern: ['Escape'],
|
||||
action: () => this.closeSidebar(),
|
||||
},
|
||||
];
|
||||
|
||||
//#endregion Services
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { attr } from '@ember-data/model';
|
||||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import { fragment } from 'ember-data-model-fragments/attributes';
|
||||
import { computed } from '@ember/object';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
||||
@classic
|
||||
export default class Service extends Fragment {
|
||||
@attr('string') name;
|
||||
@attr('string') portLabel;
|
||||
@@ -11,8 +14,34 @@ export default class Service extends Fragment {
|
||||
@fragment('consul-connect') connect;
|
||||
@attr() groupName;
|
||||
@attr() taskName;
|
||||
|
||||
get refID() {
|
||||
return `${this.groupName || this.taskName}-${this.name}`;
|
||||
}
|
||||
@attr({ defaultValue: () => [] }) healthChecks;
|
||||
|
||||
@computed('healthChecks.[]')
|
||||
get mostRecentChecks() {
|
||||
// Get unique check names, then get the most recent one
|
||||
return this.get('healthChecks')
|
||||
.mapBy('Check')
|
||||
.uniq()
|
||||
.map((name) => {
|
||||
return this.get('healthChecks')
|
||||
.sortBy('Timestamp')
|
||||
.reverse()
|
||||
.find((x) => x.Check === name);
|
||||
})
|
||||
.sortBy('Check');
|
||||
}
|
||||
|
||||
@computed('mostRecentChecks.[]')
|
||||
get mostRecentCheckStatus() {
|
||||
// Get unique check names, then get the most recent one
|
||||
return this.get('mostRecentChecks')
|
||||
.mapBy('Status')
|
||||
.reduce((acc, curr) => {
|
||||
acc[curr] = (acc[curr] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ export default class TaskGroup extends ApplicationSerializer {
|
||||
service.GroupName = hash.Name;
|
||||
});
|
||||
}
|
||||
|
||||
// Provide EphemeralDisk to each task
|
||||
hash.Tasks.forEach((task) => {
|
||||
task.EphemeralDisk = copy(hash.EphemeralDisk);
|
||||
|
||||
@@ -13,3 +13,57 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.service-sidebar {
|
||||
.aggregate-status {
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
line-height: 16px;
|
||||
& > svg {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
td.name {
|
||||
width: 100px;
|
||||
span {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
td.status {
|
||||
span {
|
||||
display: inline-grid;
|
||||
grid-auto-flow: column;
|
||||
line-height: 16px;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
td.service-output {
|
||||
padding: 0;
|
||||
code {
|
||||
padding: 1.25em 1.5em;
|
||||
max-height: 100px;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-definitions {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.allocation-services-table {
|
||||
td svg {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
$topNavOffset: 112px;
|
||||
$subNavOffset: 49px;
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
background: #ffffff;
|
||||
@@ -6,14 +9,17 @@
|
||||
right: 0%;
|
||||
overflow-y: auto;
|
||||
bottom: 0;
|
||||
top: 112px;
|
||||
top: $topNavOffset;
|
||||
transform: translateX(100%);
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease;
|
||||
box-shadow: 6px 6px rgba(0, 0, 0, 0.06), 0px 12px 16px rgba(0, 0, 0, 0.2);
|
||||
z-index: $z-modal;
|
||||
&.open {
|
||||
transform: translateX(0%);
|
||||
box-shadow: 6px 6px rgba(0, 0, 0, 0.06), 0px 12px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
&.has-subnav {
|
||||
top: calc($topNavOffset + $subNavOffset);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -270,62 +270,49 @@
|
||||
Services
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
<ListTable @source={{this.services}} as |t|>
|
||||
<ListTable class="allocation-services-table" @source={{this.servicesWithHealthChecks}} as |t|>
|
||||
<t.head>
|
||||
<th class="is-2">
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th class="is-1">
|
||||
<th>
|
||||
Port
|
||||
</th>
|
||||
<td>
|
||||
Tags
|
||||
</td>
|
||||
<td>
|
||||
On Update
|
||||
</td>
|
||||
<td>
|
||||
Connect?
|
||||
</td>
|
||||
<td>
|
||||
Upstreams
|
||||
</td>
|
||||
<td>
|
||||
Health Check Status
|
||||
</td>
|
||||
</t.head>
|
||||
<t.body as |row|>
|
||||
<tr data-test-service>
|
||||
<tr data-test-service class="is-interactive {{if (eq this.activeService row.model) "is-active"}}"
|
||||
{{on "click" (fn this.handleServiceClick row.model)}}
|
||||
{{keyboard-shortcut
|
||||
enumerated=true
|
||||
action=(fn this.handleServiceClick row.model)
|
||||
}}
|
||||
>
|
||||
<td data-test-service-name>
|
||||
{{#if (eq row.model.provider "nomad")}}
|
||||
<FlightIcon @name="nomad-color" />
|
||||
{{else}}
|
||||
<FlightIcon @name="consul-color" />
|
||||
{{/if}}
|
||||
{{row.model.name}}
|
||||
</td>
|
||||
<td data-test-service-port>
|
||||
{{row.model.portLabel}}
|
||||
</td>
|
||||
<td data-test-service-tags class="is-long-text">
|
||||
{{join ", " row.model.tags}}
|
||||
</td>
|
||||
<td data-test-service-onupdate>
|
||||
{{row.model.onUpdate}}
|
||||
</td>
|
||||
<td data-test-service-connect>
|
||||
{{if row.model.connect "Yes" "No"}}
|
||||
</td>
|
||||
<td data-test-service-upstreams>
|
||||
{{#each
|
||||
row.model.connect.sidecarService.proxy.upstreams as |upstream|
|
||||
}}
|
||||
{{upstream.destinationName}}:{{upstream.localBindPort}}
|
||||
{{#each row.model.tags as |tag|}}
|
||||
<span class="tag">{{tag}}</span>
|
||||
{{/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}}
|
||||
<ServiceStatusBar @isNarrow={{true}} @status={{row.model.mostRecentCheckStatus}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
@@ -497,4 +484,11 @@
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<AllocationServiceSidebar
|
||||
@service={{this.activeService}}
|
||||
@allocation={{this.model}}
|
||||
@fns={{hash
|
||||
closeSidebar=this.closeSidebar
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
@@ -887,11 +887,43 @@ export default function () {
|
||||
|
||||
//#region Services
|
||||
|
||||
const allocationServiceChecksHandler = function (schema) {
|
||||
let disasters = [
|
||||
"Moon's haunted",
|
||||
'reticulating splines',
|
||||
'The operation completed unexpectedly',
|
||||
'Ran out of sriracha :(',
|
||||
'¯\\_(ツ)_/¯',
|
||||
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"\n "http://www.w3.org/TR/html4/strict.dtd">\n<html>\n <head>\n <meta http-equiv="Content-Type" content="text/html;charset=utf-8">\n <title>Error response</title>\n </head>\n <body>\n <h1>Error response</h1>\n <p>Error code: 404</p>\n <p>Message: File not found.</p>\n <p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p>\n </body>\n</html>\n',
|
||||
];
|
||||
let fakeChecks = [];
|
||||
schema.serviceFragments.all().models.forEach((frag, iter) => {
|
||||
[...Array(iter)].forEach((check, checkIter) => {
|
||||
const checkOK = faker.random.boolean();
|
||||
fakeChecks.push({
|
||||
Check: `check-${checkIter}`,
|
||||
Group: `job-name.${frag.taskGroup?.name}[1]`,
|
||||
Output: checkOK
|
||||
? 'nomad: http ok'
|
||||
: disasters[Math.floor(Math.random() * disasters.length)],
|
||||
Service: frag.name,
|
||||
Status: checkOK ? 'success' : 'failure',
|
||||
StatusCode: checkOK ? 200 : 400,
|
||||
Task: frag.task?.name,
|
||||
Timestamp: new Date().getTime(),
|
||||
});
|
||||
});
|
||||
});
|
||||
return fakeChecks;
|
||||
};
|
||||
|
||||
this.get('/job/:id/services', function (schema, { params }) {
|
||||
const { services } = schema;
|
||||
return this.serialize(services.where({ jobId: params.id }));
|
||||
});
|
||||
|
||||
this.get('/client/allocation/:id/checks', allocationServiceChecksHandler);
|
||||
|
||||
//#endregion Services
|
||||
}
|
||||
|
||||
|
||||
@@ -75,11 +75,13 @@ export default Factory.extend({
|
||||
if (task.withServices) {
|
||||
const services = server.createList('service-fragment', 1, {
|
||||
provider: 'nomad',
|
||||
taskName: task.name,
|
||||
});
|
||||
|
||||
services.push(
|
||||
server.create('service-fragment', {
|
||||
provider: 'consul',
|
||||
taskName: task.name,
|
||||
})
|
||||
);
|
||||
services.forEach((fragment) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable qunit/require-expect */
|
||||
/* Mirage fixtures are random so we can't expect a set number of assertions */
|
||||
import { run } from '@ember/runloop';
|
||||
import { currentURL } from '@ember/test-helpers';
|
||||
import { currentURL, click, visit, triggerEvent } from '@ember/test-helpers';
|
||||
import { assign } from '@ember/polyfills';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
@@ -324,22 +324,7 @@ module('Acceptance | allocation detail', function (hooks) {
|
||||
|
||||
assert.equal(renderedService.name, serverService.name);
|
||||
assert.equal(renderedService.port, serverService.portLabel);
|
||||
assert.equal(renderedService.onUpdate, serverService.onUpdate);
|
||||
assert.equal(renderedService.tags, (serverService.tags || []).join(', '));
|
||||
|
||||
assert.equal(
|
||||
renderedService.connect,
|
||||
serverService.Connect ? 'Yes' : 'No'
|
||||
);
|
||||
|
||||
const upstreams = serverService.Connect.SidecarService.Proxy.Upstreams;
|
||||
const serverUpstreamsString = upstreams
|
||||
.map(
|
||||
(upstream) => `${upstream.DestinationName}:${upstream.LocalBindPort}`
|
||||
)
|
||||
.join(' ');
|
||||
|
||||
assert.equal(renderedService.upstreams, serverUpstreamsString);
|
||||
assert.equal(renderedService.tags, (serverService.tags || []).join(' '));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -632,3 +617,80 @@ module('Acceptance | allocation detail (preemptions)', function (hooks) {
|
||||
assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown');
|
||||
});
|
||||
});
|
||||
|
||||
module('Acceptance | allocation detail (services)', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
server.create('feature', { name: 'Dynamic Application Sizing' });
|
||||
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
||||
server.createList('node', 5);
|
||||
server.createList('job', 1, { createRecommendations: true });
|
||||
server.create('job', {
|
||||
withGroupServices: true,
|
||||
withTaskServices: true,
|
||||
name: 'Service-haver',
|
||||
id: 'service-haver',
|
||||
namespaceId: 'default',
|
||||
});
|
||||
|
||||
server.db.serviceFragments.update({
|
||||
healthChecks: [
|
||||
{
|
||||
Status: 'success',
|
||||
Check: 'check1',
|
||||
Timestamp: 99,
|
||||
},
|
||||
{
|
||||
Status: 'failure',
|
||||
Check: 'check2',
|
||||
Output: 'One',
|
||||
propThatDoesntMatter:
|
||||
'this object will be ignored, since it shared a Check name with a later one.',
|
||||
Timestamp: 98,
|
||||
},
|
||||
{
|
||||
Status: 'success',
|
||||
Check: 'check2',
|
||||
Output: 'Two',
|
||||
Timestamp: 99,
|
||||
},
|
||||
{
|
||||
Status: 'failure',
|
||||
Check: 'check3',
|
||||
Output: 'Oh no!',
|
||||
Timestamp: 99,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('Allocation has a list of services with active checks', async function (assert) {
|
||||
await visit('jobs/service-haver@default');
|
||||
await click('.allocation-row');
|
||||
|
||||
assert.dom('[data-test-service]').exists();
|
||||
assert.dom('.service-sidebar').exists();
|
||||
assert.dom('.service-sidebar').doesNotHaveClass('open');
|
||||
assert
|
||||
.dom('[data-test-service-status-bar]')
|
||||
.exists('At least one allocation has service health');
|
||||
await click('[data-test-service-status-bar]');
|
||||
assert.dom('.service-sidebar').hasClass('open');
|
||||
assert
|
||||
.dom('table.health-checks tr[data-service-health="success"]')
|
||||
.exists({ count: 2 }, 'Two successful health checks');
|
||||
assert
|
||||
.dom('table.health-checks tr[data-service-health="failure"]')
|
||||
.exists({ count: 1 }, 'One failing health check');
|
||||
assert
|
||||
.dom(
|
||||
'table.health-checks tr[data-service-health="failure"] td.service-output'
|
||||
)
|
||||
.containsText('Oh no!');
|
||||
|
||||
await triggerEvent('.page-layout', 'keydown', { key: 'Escape' });
|
||||
assert.dom('.service-sidebar').doesNotHaveClass('open');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { click, render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
|
||||
|
||||
module(
|
||||
'Integration | Component | allocation-service-sidebar',
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it supports basic open/close states', async function (assert) {
|
||||
assert.expect(7);
|
||||
await componentA11yAudit(this.element, assert);
|
||||
|
||||
this.set('closeSidebar', () => this.set('service', null));
|
||||
|
||||
this.set('service', { name: 'Funky Service' });
|
||||
await render(
|
||||
hbs`<AllocationServiceSidebar @service={{this.service}} @fns={{hash closeSidebar=this.closeSidebar}} />`
|
||||
);
|
||||
assert.dom('h1').includesText('Funky Service');
|
||||
assert.dom('.sidebar').hasClass('open');
|
||||
|
||||
this.set('service', null);
|
||||
await render(
|
||||
hbs`<AllocationServiceSidebar @service={{this.service}} @fns={{hash closeSidebar=this.closeSidebar}} />`
|
||||
);
|
||||
assert.dom(this.element).hasText('');
|
||||
assert.dom('.sidebar').doesNotHaveClass('open');
|
||||
|
||||
this.set('service', { name: 'Funky Service' });
|
||||
await click('[data-test-close-service-sidebar]');
|
||||
assert.dom(this.element).hasText('');
|
||||
assert.dom('.sidebar').doesNotHaveClass('open');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -12,29 +12,18 @@ module('Integration | Component | Service Status Bar', function (hooks) {
|
||||
const component = this;
|
||||
await componentA11yAudit(component, assert);
|
||||
|
||||
const healthyService = {
|
||||
const serviceStatus = {
|
||||
success: 1,
|
||||
};
|
||||
|
||||
const failingService = {
|
||||
pending: 1,
|
||||
failure: 1,
|
||||
};
|
||||
|
||||
const pendingService = {
|
||||
pending: 1,
|
||||
};
|
||||
|
||||
const services = new Map();
|
||||
services.set('peter', healthyService);
|
||||
services.set('peter', { ...services.get('peter'), ...failingService });
|
||||
services.set('peter', { ...services.get('peter'), ...pendingService });
|
||||
|
||||
this.set('services', services);
|
||||
this.set('serviceStatus', serviceStatus);
|
||||
|
||||
await render(hbs`
|
||||
<div class="inline-chart">
|
||||
<ServiceStatusBar
|
||||
@services={{this.services}}
|
||||
@status={{this.serviceStatus}}
|
||||
@name="peter"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,39 +12,86 @@ module('Unit | Controller | allocations/allocation/index', function (hooks) {
|
||||
|
||||
controller.set('model', Allocation);
|
||||
|
||||
const result = new Map();
|
||||
result.set('fakepy-fake-py', {
|
||||
failure: 1,
|
||||
success: 1,
|
||||
});
|
||||
result.set('http.server-task-fake-py', {
|
||||
failure: 1,
|
||||
success: 1,
|
||||
});
|
||||
result.set('http.server-web', {
|
||||
success: 1,
|
||||
});
|
||||
const groupFakePy = {
|
||||
refID: 'fakepy-group-fake-py',
|
||||
statuses: {
|
||||
success: 1,
|
||||
failure: 1,
|
||||
pending: 0,
|
||||
},
|
||||
};
|
||||
const taskFakePy = {
|
||||
refID: 'http.server-task-fake-py',
|
||||
statuses: {
|
||||
success: 2,
|
||||
failure: 2,
|
||||
pending: 0,
|
||||
},
|
||||
};
|
||||
const pender = {
|
||||
refID: 'http.server-pender',
|
||||
statuses: {
|
||||
success: 0,
|
||||
failure: 0,
|
||||
pending: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const fakePy = controller.serviceHealthStatuses.get('fakepy-fake-py');
|
||||
const taskFakePy = controller.serviceHealthStatuses.get(
|
||||
'http.server-task-fake-py'
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', groupFakePy.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'success').length,
|
||||
groupFakePy.statuses['success']
|
||||
);
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', groupFakePy.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'failure').length,
|
||||
groupFakePy.statuses['failure']
|
||||
);
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', groupFakePy.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'pending').length,
|
||||
groupFakePy.statuses['pending']
|
||||
);
|
||||
const web = controller.serviceHealthStatuses.get('http.server-web');
|
||||
|
||||
assert.deepEqual(
|
||||
fakePy,
|
||||
result.get('fakepy-fake-py'),
|
||||
'Service Health Check data is transformed and grouped by Service name'
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', taskFakePy.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'success').length,
|
||||
taskFakePy.statuses['success']
|
||||
);
|
||||
assert.deepEqual(
|
||||
taskFakePy,
|
||||
result.get('http.server-task-fake-py'),
|
||||
'Service Health Check data is transformed and grouped by Service name'
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', taskFakePy.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'failure').length,
|
||||
taskFakePy.statuses['failure']
|
||||
);
|
||||
assert.deepEqual(
|
||||
web,
|
||||
result.get('http.server-web'),
|
||||
'Service Health Check data is transformed and grouped by Service name'
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', taskFakePy.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'pending').length,
|
||||
taskFakePy.statuses['pending']
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', pender.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'success').length,
|
||||
pender.statuses['success']
|
||||
);
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', pender.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'failure').length,
|
||||
pender.statuses['failure']
|
||||
);
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', pender.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'pending').length,
|
||||
pender.statuses['pending']
|
||||
);
|
||||
});
|
||||
|
||||
@@ -53,50 +100,61 @@ module('Unit | Controller | allocations/allocation/index', function (hooks) {
|
||||
'controller:allocations/allocation/index'
|
||||
);
|
||||
|
||||
const dupeTaskLevelService =
|
||||
Allocation.allocationTaskGroup.Tasks[0].Services[0];
|
||||
dupeTaskLevelService.Name = 'fake-py';
|
||||
dupeTaskLevelService.isTaskLevel = true;
|
||||
|
||||
const healthChecks = Allocation.healthChecks;
|
||||
healthChecks['73ad9b936fb3f3cc4d7f62a1aab6de53'].Service = 'fake-py';
|
||||
healthChecks['19421ef816ae0d3eeeb81697bce0e261'].Service = 'fake-py';
|
||||
|
||||
controller.set('model', Allocation);
|
||||
|
||||
const result = new Map();
|
||||
result.set('fakepy-fake-py', {
|
||||
failure: 1,
|
||||
success: 1,
|
||||
});
|
||||
result.set('http.server-fake-py', {
|
||||
failure: 1,
|
||||
success: 1,
|
||||
});
|
||||
result.set('http.server-web', {
|
||||
success: 1,
|
||||
});
|
||||
const groupDupe = {
|
||||
refID: 'fakepy-duper',
|
||||
statuses: {
|
||||
success: 1,
|
||||
failure: 0,
|
||||
pending: 0,
|
||||
},
|
||||
};
|
||||
const taskDupe = {
|
||||
refID: 'http.server-duper',
|
||||
statuses: {
|
||||
success: 0,
|
||||
failure: 1,
|
||||
pending: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const fakePy = controller.serviceHealthStatuses.get('fakepy-fake-py');
|
||||
const taskFakePy = controller.serviceHealthStatuses.get(
|
||||
'http.server-fake-py'
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', groupDupe.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'success').length,
|
||||
groupDupe.statuses['success']
|
||||
);
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', groupDupe.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'failure').length,
|
||||
groupDupe.statuses['failure']
|
||||
);
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', groupDupe.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'pending').length,
|
||||
groupDupe.statuses['pending']
|
||||
);
|
||||
const web = controller.serviceHealthStatuses.get('http.server-web');
|
||||
|
||||
assert.deepEqual(
|
||||
fakePy,
|
||||
result.get('fakepy-fake-py'),
|
||||
'Service Health Check data is transformed and grouped by Service name'
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', taskDupe.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'success').length,
|
||||
taskDupe.statuses['success']
|
||||
);
|
||||
assert.deepEqual(
|
||||
taskFakePy,
|
||||
result.get('http.server-fake-py'),
|
||||
'Service Health Check data is transformed and grouped by Service name'
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', taskDupe.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'failure').length,
|
||||
taskDupe.statuses['failure']
|
||||
);
|
||||
assert.deepEqual(
|
||||
web,
|
||||
result.get('http.server-web'),
|
||||
'Service Health Check data is transformed and grouped by Service name'
|
||||
assert.equal(
|
||||
controller.servicesWithHealthChecks
|
||||
.findBy('refID', taskDupe.refID)
|
||||
.healthChecks.filter((check) => check.Status === 'pending').length,
|
||||
taskDupe.statuses['pending']
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -105,32 +163,36 @@ module('Unit | Controller | allocations/allocation/index', function (hooks) {
|
||||
// Using var to hoist this variable to the top of the module
|
||||
var Allocation = {
|
||||
namespace: 'default',
|
||||
name: 'trying-multi-dupes.fakepy[1]',
|
||||
taskGroupName: 'fakepy',
|
||||
resources: {
|
||||
Cpu: null,
|
||||
Memory: null,
|
||||
MemoryMax: null,
|
||||
Disk: null,
|
||||
Iops: null,
|
||||
Networks: [
|
||||
name: 'my-alloc',
|
||||
taskGroup: {
|
||||
name: 'fakepy',
|
||||
count: 3,
|
||||
services: [
|
||||
{
|
||||
Device: '',
|
||||
CIDR: '',
|
||||
IP: '127.0.0.1',
|
||||
Mode: 'host',
|
||||
MBits: 0,
|
||||
Ports: [
|
||||
{
|
||||
name: 'http',
|
||||
port: 22308,
|
||||
to: 0,
|
||||
isDynamic: true,
|
||||
},
|
||||
],
|
||||
Name: 'group-fake-py',
|
||||
refID: 'fakepy-group-fake-py',
|
||||
PortLabel: 'http',
|
||||
Tags: [],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
GroupName: 'fakepy',
|
||||
TaskName: '',
|
||||
healthChecks: [],
|
||||
},
|
||||
{
|
||||
Name: 'duper',
|
||||
refID: 'fakepy-duper',
|
||||
PortLabel: 'http',
|
||||
Tags: [],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
GroupName: 'fakepy',
|
||||
TaskName: '',
|
||||
healthChecks: [],
|
||||
},
|
||||
],
|
||||
Ports: [],
|
||||
},
|
||||
allocatedResources: {
|
||||
Cpu: 100,
|
||||
@@ -164,53 +226,35 @@ var Allocation = {
|
||||
},
|
||||
],
|
||||
},
|
||||
jobVersion: 0,
|
||||
modifyIndex: 31,
|
||||
modifyTime: '2022-08-29T14:13:57.761Z',
|
||||
createIndex: 15,
|
||||
createTime: '2022-08-29T14:08:57.587Z',
|
||||
clientStatus: 'running',
|
||||
desiredStatus: 'run',
|
||||
healthChecks: {
|
||||
'93a090236c79d964d1381cb218efc0f5': {
|
||||
Check: 'happy',
|
||||
c97fda942e772b43a5a537e5b0c8544c: {
|
||||
Check: 'service: "task-fake-py" check',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: '93a090236c79d964d1381cb218efc0f5',
|
||||
ID: 'c97fda942e772b43a5a537e5b0c8544c',
|
||||
Mode: 'healthiness',
|
||||
Output: 'nomad: http ok',
|
||||
Service: 'fake-py',
|
||||
Service: 'task-fake-py',
|
||||
Status: 'success',
|
||||
StatusCode: 200,
|
||||
Timestamp: 1661787992,
|
||||
Task: 'http.server',
|
||||
Timestamp: 1662131947,
|
||||
},
|
||||
'4b5daa12d4159bcb367aac65548f48f4': {
|
||||
Check: 'sad',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: '4b5daa12d4159bcb367aac65548f48f4',
|
||||
Mode: 'healthiness',
|
||||
Output:
|
||||
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"\n "http://www.w3.org/TR/html4/strict.dtd">\n<html>\n <head>\n <meta http-equiv="Content-Type" content="text/html;charset=utf-8">\n <title>Error response</title>\n </head>\n <body>\n <h1>Error response</h1>\n <p>Error code: 404</p>\n <p>Message: File not found.</p>\n <p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p>\n </body>\n</html>\n',
|
||||
Service: 'fake-py',
|
||||
Status: 'failure',
|
||||
StatusCode: 404,
|
||||
Timestamp: 1661787965,
|
||||
},
|
||||
'73ad9b936fb3f3cc4d7f62a1aab6de53': {
|
||||
'2e1bfc8ecc485ee86b972ae08e890152': {
|
||||
Check: 'task-happy',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: '73ad9b936fb3f3cc4d7f62a1aab6de53',
|
||||
ID: '2e1bfc8ecc485ee86b972ae08e890152',
|
||||
Mode: 'healthiness',
|
||||
Output: 'nomad: http ok',
|
||||
Service: 'task-fake-py',
|
||||
Status: 'success',
|
||||
StatusCode: 200,
|
||||
Task: 'http.server',
|
||||
Timestamp: 1661787992,
|
||||
Timestamp: 1662131949,
|
||||
},
|
||||
'19421ef816ae0d3eeeb81697bce0e261': {
|
||||
'6162723ab20b268c25eda69b400dc9c6': {
|
||||
Check: 'task-sad',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: '19421ef816ae0d3eeeb81697bce0e261',
|
||||
ID: '6162723ab20b268c25eda69b400dc9c6',
|
||||
Mode: 'healthiness',
|
||||
Output:
|
||||
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"\n "http://www.w3.org/TR/html4/strict.dtd">\n<html>\n <head>\n <meta http-equiv="Content-Type" content="text/html;charset=utf-8">\n <title>Error response</title>\n </head>\n <body>\n <h1>Error response</h1>\n <p>Error code: 404</p>\n <p>Message: File not found.</p>\n <p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p>\n </body>\n</html>\n',
|
||||
@@ -218,140 +262,157 @@ var Allocation = {
|
||||
Status: 'failure',
|
||||
StatusCode: 404,
|
||||
Task: 'http.server',
|
||||
Timestamp: 1661787965,
|
||||
Timestamp: 1662131936,
|
||||
},
|
||||
'784d40e33fa4c960355bbda79fbd20f0': {
|
||||
a4a7050175a2b236edcf613cb3563753: {
|
||||
Check: 'task-sad2',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: 'a4a7050175a2b236edcf613cb3563753',
|
||||
Mode: 'healthiness',
|
||||
Output:
|
||||
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"\n "http://www.w3.org/TR/html4/strict.dtd">\n<html>\n <head>\n <meta http-equiv="Content-Type" content="text/html;charset=utf-8">\n <title>Error response</title>\n </head>\n <body>\n <h1>Error response</h1>\n <p>Error code: 404</p>\n <p>Message: File not found.</p>\n <p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p>\n </body>\n</html>\n',
|
||||
Service: 'task-fake-py',
|
||||
Status: 'failure',
|
||||
StatusCode: 404,
|
||||
Task: 'http.server',
|
||||
Timestamp: 1662131936,
|
||||
},
|
||||
'2dfe58eb841bdfa704f0ae9ef5b5af5e': {
|
||||
Check: 'tcp_probe',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: '784d40e33fa4c960355bbda79fbd20f0',
|
||||
ID: '2dfe58eb841bdfa704f0ae9ef5b5af5e',
|
||||
Mode: 'readiness',
|
||||
Output: 'nomad: tcp ok',
|
||||
Service: 'web',
|
||||
Status: 'success',
|
||||
Task: 'http.server',
|
||||
Timestamp: 1661787995,
|
||||
Timestamp: 1662131949,
|
||||
},
|
||||
'69021054964f4c461b3c4c4f456e16a8': {
|
||||
Check: 'happy',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: '69021054964f4c461b3c4c4f456e16a8',
|
||||
Mode: 'healthiness',
|
||||
Output: 'nomad: http ok',
|
||||
Service: 'group-fake-py',
|
||||
Status: 'success',
|
||||
StatusCode: 200,
|
||||
Timestamp: 1662131949,
|
||||
},
|
||||
'913f5b725ceecdd5ff48a9a51ddf8513': {
|
||||
Check: 'sad',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: '913f5b725ceecdd5ff48a9a51ddf8513',
|
||||
Mode: 'healthiness',
|
||||
Output:
|
||||
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"\n "http://www.w3.org/TR/html4/strict.dtd">\n<html>\n <head>\n <meta http-equiv="Content-Type" content="text/html;charset=utf-8">\n <title>Error response</title>\n </head>\n <body>\n <h1>Error response</h1>\n <p>Error code: 404</p>\n <p>Message: File not found.</p>\n <p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p>\n </body>\n</html>\n',
|
||||
Service: 'group-fake-py',
|
||||
Status: 'failure',
|
||||
StatusCode: 404,
|
||||
Timestamp: 1662131936,
|
||||
},
|
||||
bloop: {
|
||||
Check: 'is-alive',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: 'bloop',
|
||||
Mode: 'healthiness',
|
||||
Service: 'pender',
|
||||
Status: 'pending',
|
||||
Task: 'http.server',
|
||||
Timestamp: 1662131947,
|
||||
},
|
||||
'group-dupe': {
|
||||
Check: 'is-alive',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: 'group-dupe',
|
||||
Mode: 'healthiness',
|
||||
Service: 'duper',
|
||||
Status: 'success',
|
||||
Task: '',
|
||||
Timestamp: 1662131947,
|
||||
},
|
||||
'task-dupe': {
|
||||
Check: 'is-alive',
|
||||
Group: 'trying-multi-dupes.fakepy[1]',
|
||||
ID: 'task-dupe',
|
||||
Mode: 'healthiness',
|
||||
Service: 'duper',
|
||||
Status: 'failure',
|
||||
Task: 'http.server',
|
||||
Timestamp: 1662131947,
|
||||
},
|
||||
},
|
||||
isMigrating: false,
|
||||
wasPreempted: false,
|
||||
allocationTaskGroup: {
|
||||
Name: 'fakepy',
|
||||
Count: 3,
|
||||
Tasks: [
|
||||
{
|
||||
Name: 'http.server',
|
||||
Driver: 'raw_exec',
|
||||
Kind: '',
|
||||
Meta: null,
|
||||
Lifecycle: null,
|
||||
ReservedMemory: 300,
|
||||
ReservedMemoryMax: 0,
|
||||
ReservedCPU: 100,
|
||||
ReservedDisk: 0,
|
||||
ReservedEphemeralDisk: 300,
|
||||
Services: [
|
||||
{
|
||||
Name: 'task-fake-py',
|
||||
PortLabel: 'http',
|
||||
Tags: [],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
},
|
||||
{
|
||||
Name: 'web',
|
||||
PortLabel: 'http',
|
||||
Tags: ['web', 'tcp', 'lol', 'lmao'],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
},
|
||||
{
|
||||
Name: 'duper',
|
||||
PortLabel: 'http',
|
||||
Tags: ['web', 'tcp', 'lol', 'lmao'],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
},
|
||||
],
|
||||
VolumeMounts: null,
|
||||
},
|
||||
],
|
||||
Services: [
|
||||
{
|
||||
Name: 'fake-py',
|
||||
PortLabel: 'http',
|
||||
Tags: [],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
},
|
||||
{
|
||||
Name: 'duper',
|
||||
PortLabel: 'http',
|
||||
Tags: [],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
},
|
||||
],
|
||||
Volumes: [],
|
||||
Scaling: null,
|
||||
Meta: null,
|
||||
ReservedEphemeralDisk: 300,
|
||||
},
|
||||
|
||||
states: [
|
||||
{
|
||||
Name: 'http.server',
|
||||
State: 'running',
|
||||
StartedAt: '2022-08-29T14:08:57.680Z',
|
||||
FinishedAt: null,
|
||||
Failed: false,
|
||||
Resources: {
|
||||
Cpu: 100,
|
||||
Memory: 300,
|
||||
MemoryMax: null,
|
||||
Disk: null,
|
||||
Iops: null,
|
||||
Networks: [],
|
||||
Ports: [],
|
||||
task: {
|
||||
name: 'http.server',
|
||||
driver: 'raw_exec',
|
||||
kind: '',
|
||||
meta: null,
|
||||
lifecycle: null,
|
||||
reservedMemory: 300,
|
||||
reservedMemoryMax: 0,
|
||||
reservedCPU: 100,
|
||||
reservedDisk: 0,
|
||||
reservedEphemeralDisk: 300,
|
||||
services: [
|
||||
{
|
||||
Name: 'task-fake-py',
|
||||
PortLabel: 'http',
|
||||
refID: 'http.server-task-fake-py',
|
||||
Tags: [
|
||||
'long',
|
||||
'and',
|
||||
'arbitrary',
|
||||
'list',
|
||||
'of',
|
||||
'tags',
|
||||
'arbitrary',
|
||||
],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
TaskName: 'http.server',
|
||||
healthChecks: [],
|
||||
},
|
||||
{
|
||||
Name: 'pender',
|
||||
refID: 'http.server-pender',
|
||||
PortLabel: 'http',
|
||||
Tags: ['lol', 'lmao'],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
TaskName: 'http.server',
|
||||
healthChecks: [],
|
||||
},
|
||||
{
|
||||
Name: 'web',
|
||||
refID: 'http.server-web',
|
||||
PortLabel: 'http',
|
||||
Tags: ['lol', 'lmao'],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
TaskName: 'http.server',
|
||||
healthChecks: [],
|
||||
},
|
||||
{
|
||||
Name: 'duper',
|
||||
refID: 'http.server-duper',
|
||||
PortLabel: 'http',
|
||||
Tags: ['lol', 'lmao'],
|
||||
OnUpdate: 'require_healthy',
|
||||
Provider: 'nomad',
|
||||
Connect: null,
|
||||
TaskName: 'http.server',
|
||||
healthChecks: [],
|
||||
},
|
||||
],
|
||||
volumeMounts: null,
|
||||
},
|
||||
Events: [
|
||||
{
|
||||
Type: 'Received',
|
||||
Signal: 0,
|
||||
ExitCode: 0,
|
||||
Time: '2022-08-29T14:08:57.592Z',
|
||||
TimeNanos: 865024,
|
||||
DisplayMessage: 'Task received by client',
|
||||
},
|
||||
{
|
||||
Type: 'Task Setup',
|
||||
Signal: 0,
|
||||
ExitCode: 0,
|
||||
Time: '2022-08-29T14:08:57.595Z',
|
||||
TimeNanos: 160064,
|
||||
DisplayMessage: 'Building Task Directory',
|
||||
},
|
||||
{
|
||||
Type: 'Started',
|
||||
Signal: 0,
|
||||
ExitCode: 0,
|
||||
Time: '2022-08-29T14:08:57.680Z',
|
||||
TimeNanos: 728064,
|
||||
DisplayMessage: 'Task started by client',
|
||||
},
|
||||
{
|
||||
Type: 'Alloc Unhealthy',
|
||||
Signal: 0,
|
||||
ExitCode: 0,
|
||||
Time: '2022-08-29T14:13:57.592Z',
|
||||
TimeNanos: 152064,
|
||||
DisplayMessage:
|
||||
'Task not running for min_healthy_time of 10s by healthy_deadline of 5m0s',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
rescheduleEvents: [],
|
||||
|
||||
Reference in New Issue
Block a user