underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
711 lines (630 loc) • 30.1 kB
JavaScript
'use strict';
import { expect } from 'chai';
import fs from 'fs-extra';
import {
curlStatusChainFactory,
hostRenderInstancesFactory,
instanceTrafficPlanFactory,
isTrafficServingFactory,
hostIngressFactsFactory,
nextTrafficFactory,
schedulableNodeFactory,
stopPlanFactory,
trafficFromRoutingInfoFactory,
trafficProbePathsFactory,
trafficTableRowsFactory,
} from '../src/server/conf.js';
// Pure resolution over the conf: the cluster lookups are injected, so every case
// here fixes the routed colour and the endpoint readiness explicitly.
const INSTANCES = [
{ id: 'mmo-server', host: 'server.fixture.test', path: '/' },
{ id: 'mmo-server-forest', host: 'server.fixture.test', path: '/FOREST' },
];
const planFactory = ({ live = {}, ready = {}, requestedTraffic = '', instances = INSTANCES } = {}) =>
instanceTrafficPlanFactory({
instances,
requestedTraffic,
liveTrafficOf: (instance) => live[instance.id],
servesTraffic: (instance, colour) => ready[`${instance.id}:${colour}`] === true,
});
describe('blue/green traffic plan', () => {
describe('curlStatusChainFactory', () => {
it('extracts every followed response without duplicating -i headers', () => {
const raw = [
'< HTTP/1.1 301 Moved Permanently',
'HTTP/1.1 301 Moved Permanently',
'< HTTP/2 200',
'HTTP/2 200',
'UNDERPOST_CURL_FINAL=200',
].join('\n');
expect(curlStatusChainFactory(raw)).to.deep.equal(['301', '200']);
});
it('ignores proxy CONNECT acknowledgements and reports no-response as 000', () => {
expect(curlStatusChainFactory('< HTTP/1.1 200 Connection established\nUNDERPOST_CURL_FINAL=000')).to.deep.equal([
'000',
]);
});
it('uses curl write-out when no verbose response line was emitted', () => {
expect(curlStatusChainFactory('UNDERPOST_CURL_FINAL=204')).to.deep.equal(['204']);
});
});
describe('nextTrafficFactory', () => {
it('flips the live colour', () => {
expect(nextTrafficFactory('blue')).to.equal('green');
expect(nextTrafficFactory('green')).to.equal('blue');
});
it('starts on blue when no colour is live', () => {
for (const live of ['', undefined, null]) expect(nextTrafficFactory(live)).to.equal('blue');
});
it('honours an explicitly requested colour over the flip', () => {
expect(nextTrafficFactory('blue', 'blue')).to.equal('blue');
expect(nextTrafficFactory('green', 'green')).to.equal('green');
});
it('ignores a request that is not a colour', () => {
for (const requested of ['', 'red', 'BLUE', undefined])
expect(nextTrafficFactory('blue', requested)).to.equal('green');
});
});
// `run sync` gates its no-backend checkpoint on this directly; `run instance`
// reaches it through instanceTrafficPlanFactory. One definition, so neither
// runner can decide differently about taking a live host offline.
describe('isTrafficServingFactory', () => {
it('is serving only when the routed colour is also reachable', () => {
expect(isTrafficServingFactory({ liveTraffic: 'blue', hasReadyEndpoints: () => true })).to.equal(true);
expect(isTrafficServingFactory({ liveTraffic: 'blue', hasReadyEndpoints: () => false })).to.equal(false);
});
it('is not serving when no colour is routed', () => {
for (const liveTraffic of ['', undefined, null])
expect(isTrafficServingFactory({ liveTraffic, hasReadyEndpoints: () => true })).to.equal(false);
});
it('asks about the routed colour, and only when one is routed', () => {
const asked = [];
isTrafficServingFactory({
liveTraffic: 'green',
hasReadyEndpoints: (colour) => {
asked.push(colour);
return true;
},
});
isTrafficServingFactory({
liveTraffic: '',
hasReadyEndpoints: (colour) => {
asked.push(colour);
return true;
},
});
expect(asked).to.deep.equal(['green']);
});
it('defaults to not serving with nothing supplied', () => {
expect(isTrafficServingFactory({})).to.equal(false);
});
});
describe('instanceTrafficPlanFactory', () => {
it('flips every instance off the colour it serves', () => {
const { liveTrafficById, targetTrafficById } = planFactory({
live: { 'mmo-server': 'blue', 'mmo-server-forest': 'green' },
ready: { 'mmo-server:blue': true, 'mmo-server-forest:green': true },
});
expect(liveTrafficById).to.deep.equal({ 'mmo-server': 'blue', 'mmo-server-forest': 'green' });
expect(targetTrafficById).to.deep.equal({ 'mmo-server': 'green', 'mmo-server-forest': 'blue' });
});
// The regression this guards: a re-deploy of a healthy host must not be
// mistaken for a first bring-up, because the no-backend fallback checkpoint
// gated on `serving` routes the edge at a colour that has no Deployment.
it('reports a routed colour with a ready endpoint as serving', () => {
const { serving } = planFactory({
live: { 'mmo-server': 'blue', 'mmo-server-forest': 'blue' },
ready: { 'mmo-server:blue': true, 'mmo-server-forest:blue': true },
});
expect(serving.map((instance) => instance.id)).to.deep.equal(['mmo-server', 'mmo-server-forest']);
});
it('reports nothing serving on a first bring-up', () => {
const { serving, targetTrafficById } = planFactory({ live: {}, ready: {} });
expect(serving).to.deep.equal([]);
expect(targetTrafficById).to.deep.equal({ 'mmo-server': 'blue', 'mmo-server-forest': 'blue' });
});
// A route can still name a colour whose workload is long gone; that is a
// dead route, not traffic, so the checkpoint is free to run.
it('does not treat a routed colour without a ready endpoint as serving', () => {
const { serving, targetTrafficById } = planFactory({
live: { 'mmo-server': 'blue', 'mmo-server-forest': 'blue' },
ready: {},
});
expect(serving).to.deep.equal([]);
expect(targetTrafficById).to.deep.equal({ 'mmo-server': 'green', 'mmo-server-forest': 'green' });
});
it('counts only the instances that are actually serving', () => {
const { serving } = planFactory({
live: { 'mmo-server': 'blue', 'mmo-server-forest': 'blue' },
ready: { 'mmo-server:blue': true },
});
expect(serving.map((instance) => instance.id)).to.deep.equal(['mmo-server']);
});
// Readiness is asked about the live colour only. An idle colour left Ready by
// a previous cycle is not what the host is serving on.
it('ignores readiness of a colour that is not routed', () => {
const { serving } = planFactory({
live: { 'mmo-server': 'blue' },
ready: { 'mmo-server:green': true },
instances: [INSTANCES[0]],
});
expect(serving).to.deep.equal([]);
});
it('never consults readiness when no colour is routed', () => {
const asked = [];
const { serving } = instanceTrafficPlanFactory({
instances: INSTANCES,
liveTrafficOf: () => '',
servesTraffic: (instance, colour) => {
asked.push(`${instance.id}:${colour}`);
return true;
},
});
expect(asked).to.deep.equal([]);
expect(serving).to.deep.equal([]);
});
it('applies a requested colour to every instance', () => {
const { targetTrafficById } = planFactory({
live: { 'mmo-server': 'blue', 'mmo-server-forest': 'green' },
requestedTraffic: 'blue',
});
expect(targetTrafficById).to.deep.equal({ 'mmo-server': 'blue', 'mmo-server-forest': 'blue' });
});
it('is a no-op with no instances', () => {
expect(instanceTrafficPlanFactory({})).to.deep.equal({
liveTrafficById: {},
targetTrafficById: {},
serving: [],
});
});
});
// A host's server block and HTTPRoute are single shared objects, so what the
// render is built from decides which variants keep a route at all.
describe('hostRenderInstancesFactory', () => {
const MAIN = INSTANCES[0];
const FOREST = INSTANCES[1];
const TEST = { id: 'mmo-server-test', host: 'server.fixture.test', path: '/TEST' };
// The reported regression: trimming the conf to one variant must not delete
// the routes of the variants still deployed behind the same host.
it('keeps a still-deployed variant that the conf no longer declares', () => {
const rendered = hostRenderInstancesFactory({
declared: [FOREST],
preserved: [MAIN, FOREST, TEST],
isDeployed: () => true,
});
expect(rendered.map((instance) => instance.id)).to.deep.equal([
'mmo-server-forest',
'mmo-server',
'mmo-server-test',
]);
});
it('drops a preserved variant once its workload is gone', () => {
const rendered = hostRenderInstancesFactory({
declared: [FOREST],
preserved: [MAIN, TEST],
isDeployed: (instance) => instance.id === 'mmo-server',
});
expect(rendered.map((instance) => instance.id)).to.deep.equal(['mmo-server-forest', 'mmo-server']);
});
it('never duplicates a variant that is both declared and preserved', () => {
const rendered = hostRenderInstancesFactory({
declared: [MAIN, FOREST],
preserved: [MAIN, FOREST],
isDeployed: () => true,
});
expect(rendered.map((instance) => instance.id)).to.deep.equal(['mmo-server', 'mmo-server-forest']);
});
// The declared entry is the current truth for that id; a stale preserved copy
// of the same id must not shadow it.
it('prefers the declared entry over a preserved one of the same id', () => {
const rendered = hostRenderInstancesFactory({
declared: [{ ...FOREST, path: '/FOREST-NEW' }],
preserved: [{ ...FOREST, path: '/FOREST-OLD' }],
isDeployed: () => true,
});
expect(rendered).to.have.lengthOf(1);
expect(rendered[0].path).to.equal('/FOREST-NEW');
});
it('ignores preserved entries with no id', () => {
expect(
hostRenderInstancesFactory({ declared: [FOREST], preserved: [{}, null], isDeployed: () => true }),
).to.deep.equal([FOREST]);
});
it('is the declared set when nothing was preserved', () => {
expect(hostRenderInstancesFactory({ declared: [MAIN, FOREST] })).to.deep.equal([MAIN, FOREST]);
expect(hostRenderInstancesFactory({})).to.deep.equal([]);
});
});
// A nodeSelector naming a node that does not exist does not degrade — nothing
// schedules — so the guessed default has to be checked against the cluster.
describe('schedulableNodeFactory', () => {
const KUBEADM = [{ NAME: 'localhost.localdomain', STATUS: 'Ready', ROLES: 'control-plane' }];
const KIND = [
{ NAME: 'kind-control-plane', STATUS: 'Ready', ROLES: 'control-plane' },
{ NAME: 'kind-worker', STATUS: 'Ready', ROLES: '<none>' },
];
// The reported failure: `--dev` on a kubeadm cluster guesses `kind-worker`.
it('substitutes a real node when the guess does not exist', () => {
expect(schedulableNodeFactory({ nodes: KUBEADM, node: 'kind-worker' })).to.deep.equal({
node: 'localhost.localdomain',
corrected: true,
});
});
it('keeps a node the cluster actually has', () => {
expect(schedulableNodeFactory({ nodes: KIND, node: 'kind-worker' })).to.deep.equal({
node: 'kind-worker',
corrected: false,
});
});
it('prefers a worker over the control plane', () => {
expect(schedulableNodeFactory({ nodes: KIND, node: 'nope' }).node).to.equal('kind-worker');
});
it('falls back to the control plane when it is the only node', () => {
expect(schedulableNodeFactory({ nodes: KUBEADM, node: '' }).node).to.equal('localhost.localdomain');
});
it('skips a NotReady node but still answers when none are Ready', () => {
const nodes = [
{ NAME: 'down', STATUS: 'NotReady', ROLES: '<none>' },
{ NAME: 'up', STATUS: 'Ready', ROLES: '<none>' },
];
expect(schedulableNodeFactory({ nodes, node: 'nope' }).node).to.equal('up');
expect(schedulableNodeFactory({ nodes: [nodes[0]], node: 'nope' }).node).to.equal('down');
});
it('treats a cordoned node as Ready', () => {
const nodes = [{ NAME: 'only', STATUS: 'Ready,SchedulingDisabled', ROLES: '<none>' }];
expect(schedulableNodeFactory({ nodes, node: 'nope' }).node).to.equal('only');
});
// An unreadable cluster is not evidence the name is wrong.
it('leaves the choice untouched when there is no node list', () => {
expect(schedulableNodeFactory({ nodes: [], node: 'kind-worker' })).to.deep.equal({
node: 'kind-worker',
corrected: false,
});
expect(schedulableNodeFactory({ nodes: [{}, null], node: 'kind-worker' }).corrected).to.equal(false);
});
});
describe('stopPlanFactory', () => {
const FAMILY = {
'mmo-server': [
{ id: 'mmo-server', host: 'server.fixture.test' },
{ id: 'mmo-server-forest', host: 'server.fixture.test' },
],
'mmo-client': [{ id: 'mmo-client', host: 'client.fixture.test' }],
};
const plan = (input) =>
stopPlanFactory({
env: 'development',
instancesFor: (instanceId) => FAMILY[instanceId] || [],
liveTrafficOf: () => 'blue',
...input,
});
const names = (input) => plan(input).deployments.map((target) => target.deployment);
// 1 — default is the partner of whatever is serving.
it('stops the inactive colour of the PWA workload', () => {
expect(names({ deployId: 'dd-cyberia' })).to.deep.equal(['dd-cyberia-development-green']);
});
it('stops the colours named by --traffic, both when both are given', () => {
expect(names({ deployId: 'dd-cyberia', traffic: 'blue,green' })).to.deep.equal([
'dd-cyberia-development-blue',
'dd-cyberia-development-green',
]);
expect(names({ deployId: 'dd-cyberia', traffic: 'blue' })).to.deep.equal(['dd-cyberia-development-blue']);
});
it('refuses a --traffic value that is not a colour', () => {
expect(plan({ deployId: 'dd-cyberia', traffic: 'red' }).error).to.match(/--traffic accepts blue and\/or green/);
});
// 2 — a literal path names the object outright and overrides every flag.
it('stops exactly the literal deployment, ignoring flags', () => {
const result = plan({
path: 'dd-cyberia-mmo-server-forest-development-blue',
deployId: 'dd-other',
instanceId: 'mmo-client',
traffic: 'blue,green',
});
expect(result.deployments.map((target) => target.deployment)).to.deep.equal([
'dd-cyberia-mmo-server-forest-development-blue',
]);
expect(result.deployments[0].kind).to.equal('literal');
});
it('accepts several literal deployments', () => {
expect(names({ path: 'a-development-blue, b-development-green' })).to.deep.equal([
'a-development-blue',
'b-development-green',
]);
});
// 3 + 4 — instance ids add their whole family alongside the PWA workload.
it('adds every variant of each instance family', () => {
expect(names({ deployId: 'dd-cyberia', instanceId: 'mmo-client,mmo-server' })).to.deep.equal([
'dd-cyberia-development-green',
'dd-cyberia-mmo-client-development-green',
'dd-cyberia-mmo-server-development-green',
'dd-cyberia-mmo-server-forest-development-green',
]);
});
it('applies explicit colours to the PWA workload and every instance alike', () => {
expect(names({ deployId: 'dd-cyberia', instanceId: 'mmo-client', traffic: 'blue,green' })).to.deep.equal([
'dd-cyberia-development-blue',
'dd-cyberia-development-green',
'dd-cyberia-mmo-client-development-blue',
'dd-cyberia-mmo-client-development-green',
]);
});
it('resolves each target its own colour', () => {
const deployments = plan({
deployId: 'dd-cyberia',
instanceId: 'mmo-server',
liveTrafficOf: (target) => (target.id === 'dd-cyberia-mmo-server' ? 'green' : 'blue'),
}).deployments;
expect(deployments.map((target) => target.deployment)).to.deep.equal([
'dd-cyberia-development-green',
'dd-cyberia-mmo-server-development-blue',
'dd-cyberia-mmo-server-forest-development-green',
]);
});
it('never plans the same deployment twice', () => {
expect(names({ deployId: 'dd-cyberia', instanceId: 'mmo-server,mmo-server' })).to.deep.equal([
'dd-cyberia-development-green',
'dd-cyberia-mmo-server-development-green',
'dd-cyberia-mmo-server-forest-development-green',
]);
});
// 5 — an instance id is only unique inside a deploy.
it('refuses --instance-id without --deploy-id', () => {
const result = plan({ instanceId: 'mmo-server' });
expect(result.error).to.match(/--instance-id requires --deploy-id/);
expect(result.deployments).to.deep.equal([]);
});
it('refuses a plan with nothing to act on', () => {
expect(plan({}).error).to.match(/nothing to stop/);
});
it('passes the instance host through for colour resolution', () => {
const asked = [];
plan({
deployId: 'dd-cyberia',
instanceId: 'mmo-client',
liveTrafficOf: (target) => {
asked.push([target.id, target.host, target.kind]);
return 'blue';
},
});
expect(asked).to.deep.equal([
['dd-cyberia', '', 'pwa'],
['dd-cyberia-mmo-client', 'client.fixture.test', 'instance'],
]);
});
});
// One host's routing text is matched against several deployments and both
// environments, so the match has to be exact about which name it is reading.
describe('trafficFromRoutingInfoFactory', () => {
const INFO = [
'name: dd-cyberia-mmo-server-development-green-service',
'name: dd-cyberia-mmo-server-forest-development-blue-service',
'name: dd-cyberia-development-blue-service',
].join('\n');
it('reads each deployment its own colour from a shared host', () => {
expect(trafficFromRoutingInfoFactory({ info: INFO, deployId: 'dd-cyberia', env: 'development' })).to.equal(
'blue',
);
expect(
trafficFromRoutingInfoFactory({ info: INFO, deployId: 'dd-cyberia-mmo-server', env: 'development' }),
).to.equal('green');
expect(
trafficFromRoutingInfoFactory({ info: INFO, deployId: 'dd-cyberia-mmo-server-forest', env: 'development' }),
).to.equal('blue');
});
it('reads the colour from the stable Service selector', () => {
expect(
trafficFromRoutingInfoFactory({
info: 'dd-cyberia-development-green',
deployId: 'dd-cyberia',
env: 'development',
}),
).to.equal('green');
});
// The reported failure: a development cluster reported as entirely unrouted
// because every match was anchored on `-production-`.
it('finds nothing for an environment the routing does not mention', () => {
expect(trafficFromRoutingInfoFactory({ info: INFO, deployId: 'dd-cyberia', env: 'production' })).to.equal(null);
});
it('is null when there is no routing text', () => {
for (const info of ['', ' \n ', undefined])
expect(trafficFromRoutingInfoFactory({ info, deployId: 'dd-cyberia', env: 'development' })).to.equal(null);
});
it('does not let a regex metacharacter in the id match loosely', () => {
expect(
trafficFromRoutingInfoFactory({
info: 'name: dd-any-development-blue-service',
deployId: 'dd.any',
env: 'development',
}),
).to.equal(null);
});
it('falls back to a whole-text match with no environment', () => {
expect(trafficFromRoutingInfoFactory({ info: 'routes to green here', deployId: 'dd-cyberia' })).to.equal('green');
expect(trafficFromRoutingInfoFactory({ info: 'nothing routed', deployId: 'dd-cyberia' })).to.equal(null);
});
});
// None of these three is readable from the route object alone, which is why
// the report correlates four kinds instead of reading one.
describe('hostIngressFactsFactory', () => {
const GATEWAYS = [
{
metadata: { name: 'dd-cyberia-development' },
spec: {
listeners: [
{ name: 'http', protocol: 'HTTP' },
{ name: 'https', protocol: 'HTTPS', tls: { certificateRefs: [{ name: 'underpost.net' }] } },
],
},
},
{ metadata: { name: 'dd-plain-development' }, spec: { listeners: [{ name: 'http', protocol: 'HTTP' }] } },
];
const POLICIES = [
{ spec: { targetRefs: [{ kind: 'Gateway', name: 'dd-cyberia-development', sectionName: 'https' }], http3: {} } },
];
it('reads the kind, TLS and HTTP/3 for a Gateway API host', () => {
const facts = hostIngressFactsFactory({
httpRoutes: [{ spec: { hostnames: ['underpost.net'], parentRefs: [{ name: 'dd-cyberia-development' }] } }],
gateways: GATEWAYS,
clientTrafficPolicies: POLICIES,
});
expect(facts['underpost.net']).to.deep.equal({ route: 'HTTPRoute', tls: true, http3: true });
});
it('reads TLS from an HTTPProxy virtualhost, and never claims HTTP/3 for it', () => {
const facts = hostIngressFactsFactory({
httpProxies: [
{ spec: { virtualhost: { fqdn: 'legacy.test', tls: { secretName: 'legacy' } } } },
{ spec: { virtualhost: { fqdn: 'plain.test' } } },
],
});
expect(facts['legacy.test']).to.deep.equal({ route: 'HTTPProxy', tls: true, http3: false });
expect(facts['plain.test']).to.deep.equal({ route: 'HTTPProxy', tls: false, http3: false });
});
it('reports no TLS for a Gateway with only a plain HTTP listener', () => {
const facts = hostIngressFactsFactory({
httpRoutes: [{ spec: { hostnames: ['plain.test'], parentRefs: [{ name: 'dd-plain-development' }] } }],
gateways: GATEWAYS,
});
expect(facts['plain.test']).to.deep.equal({ route: 'HTTPRoute', tls: false, http3: false });
});
// QUIC only exists where TLS does, so a policy on a plain listener describes
// nothing — the implementation rejects it too.
it('does not claim HTTP/3 from a policy targeting a Gateway with no TLS listener', () => {
const facts = hostIngressFactsFactory({
httpRoutes: [{ spec: { hostnames: ['plain.test'], parentRefs: [{ name: 'dd-plain-development' }] } }],
gateways: GATEWAYS,
clientTrafficPolicies: [{ spec: { targetRefs: [{ name: 'dd-plain-development' }], http3: {} } }],
});
expect(facts['plain.test'].http3).to.equal(false);
});
it('lets the HTTPRoute win a host described by both kinds', () => {
const facts = hostIngressFactsFactory({
httpRoutes: [{ spec: { hostnames: ['dual.test'], parentRefs: [{ name: 'dd-cyberia-development' }] } }],
httpProxies: [{ spec: { virtualhost: { fqdn: 'dual.test' } } }],
gateways: GATEWAYS,
clientTrafficPolicies: POLICIES,
});
expect(facts['dual.test']).to.deep.equal({ route: 'HTTPRoute', tls: true, http3: true });
});
it('yields nothing for missing CRDs or malformed items', () => {
expect(hostIngressFactsFactory()).to.deep.equal({});
expect(hostIngressFactsFactory({ httpProxies: [{ spec: {} }], httpRoutes: [{ spec: {} }] })).to.deep.equal({});
});
});
describe('trafficProbePathsFactory', () => {
it('probes only the canonical path when no replicas are declared', () => {
expect(trafficProbePathsFactory({}, '/wp')).to.deep.equal(['/wp']);
expect(trafficProbePathsFactory(undefined, '/wp')).to.deep.equal(['/wp']);
});
// '/' in engine-private/conf/dd-test/conf.server.json: replicas without
// singleReplica, so the canonical path is still actually served.
it('probes the canonical path plus every replica when singleReplica is not set', () => {
expect(trafficProbePathsFactory({ replicas: ['/r1'] }, '/')).to.deep.equal(['/', '/r1']);
});
// '/single-replica' in the same fixture: singleReplica means the canonical
// path is never itself served, only its replicas are.
it('probes only the replicas, dropping the canonical path, when singleReplica is set', () => {
expect(trafficProbePathsFactory({ replicas: ['/r2'], singleReplica: true }, '/single-replica')).to.deep.equal([
'/r2',
]);
});
it('treats a non-array replicas value as no replicas', () => {
expect(trafficProbePathsFactory({ replicas: 'not-an-array' }, '/wp')).to.deep.equal(['/wp']);
});
});
describe('trafficTableRowsFactory', () => {
const ENTRIES = [
{ kind: 'pwa', deployId: 'dd-a', id: 'dd-a', host: 'a.test', path: '/', deployment: 'dd-a-development' },
{
kind: 'instance',
deployId: 'dd-a',
id: 'dd-a-mmo-server',
host: 'server.test',
path: '/',
deployment: 'dd-a-mmo-server-development',
},
];
it('reports both deployment kinds with their live colour', () => {
const rows = trafficTableRowsFactory({
entries: ENTRIES,
liveTrafficOf: (entry) => (entry.kind === 'pwa' ? 'blue' : 'green'),
servesTraffic: () => true,
});
expect(rows.map((row) => [row.kind, row.traffic, row.serving])).to.deep.equal([
['pwa', 'blue', true],
['instance', 'green', true],
]);
});
it('narrows to the requested hosts', () => {
const rows = trafficTableRowsFactory({ entries: ENTRIES, hosts: ['server.test'], liveTrafficOf: () => 'blue' });
expect(rows.map((row) => row.host)).to.deep.equal(['server.test']);
});
it('reports every host when none are requested', () => {
for (const hosts of [[], ['']])
expect(trafficTableRowsFactory({ entries: ENTRIES, hosts, liveTrafficOf: () => 'blue' })).to.have.lengthOf(2);
});
// A deployment with no published route is a real state to report, not a row
// to hide: it is exactly what a half-finished promote looks like.
it('keeps an entry whose colour cannot be read', () => {
const rows = trafficTableRowsFactory({ entries: ENTRIES, liveTrafficOf: () => null, servesTraffic: () => true });
expect(rows.map((row) => [row.traffic, row.serving])).to.deep.equal([
['', false],
['', false],
]);
});
it('keeps configured report rows when no Kubernetes deployments exist', () => {
const runSource = fs.readFileSync(new URL('../src/cli/run.js', import.meta.url), 'utf8');
const start = runSource.indexOf(" 'get-traffic': async");
const end = runSource.indexOf(" 'instance-promote': async", start);
const getTraffic = runSource.slice(start, end);
expect(getTraffic).to.include('.map((entry) => ({ ...entry, env }))');
expect(getTraffic).not.to.include('.filter((entry) => deployedNames.some');
});
it('reports a routed colour with no ready endpoint as not serving', () => {
const rows = trafficTableRowsFactory({
entries: [ENTRIES[0]],
liveTrafficOf: () => 'blue',
servesTraffic: () => false,
});
expect(rows[0]).to.include({ traffic: 'blue', serving: false });
});
it('is empty with no entries', () => {
expect(trafficTableRowsFactory({})).to.deep.equal([]);
});
it('feeds the report from stable Service selectors before legacy route text', () => {
const runSource = fs.readFileSync(new URL('../src/cli/run.js', import.meta.url), 'utf8');
const start = runSource.indexOf(" 'get-traffic': async");
const end = runSource.indexOf(" 'instance-promote': async", start);
const getTraffic = runSource.slice(start, end);
const listServices = getTraffic.indexOf("listResources('service')");
const stableSelector = getTraffic.indexOf('const stableTraffic = trafficFromRoutingInfoFactory');
const legacyRoute = getTraffic.indexOf('const legacyTraffic = trafficFromRoutingInfoFactory');
expect(listServices).to.be.greaterThan(-1);
expect(stableSelector).to.be.greaterThan(listServices);
expect(legacyRoute).to.be.greaterThan(stableSelector);
});
it('uses real followed curl probes and reports current and opposite deployment status without ENV', () => {
const runSource = fs.readFileSync(new URL('../src/cli/run.js', import.meta.url), 'utf8');
const start = runSource.indexOf(" 'get-traffic': async");
const end = runSource.indexOf(" 'instance-promote': async", start);
const getTraffic = runSource.slice(start, end);
expect(getTraffic).to.include('curl -L -v -i -s');
expect(getTraffic).to.include("'OPPOSITE'");
expect(getTraffic).not.to.include('showOpposite');
expect(getTraffic).to.include("'CURRENT'");
expect(getTraffic).not.to.include("'ENV',");
expect(getTraffic).to.include('opposite,');
expect(getTraffic).to.include("status.exists ? (status.ready ? 'ready' : 'not-ready') : 'missing'");
expect(getTraffic).to.include("status.serving ? 'serving' : 'not-serving'");
expect(getTraffic).to.include("`${probe.path} [${probe.statuses.join('→')}]`");
});
it('caches stable Service readiness across hosts sharing the same deployment', () => {
const runSource = fs.readFileSync(new URL('../src/cli/run.js', import.meta.url), 'utf8');
const start = runSource.indexOf(" 'get-traffic': async");
const end = runSource.indexOf(" 'instance-promote': async", start);
const getTraffic = runSource.slice(start, end);
expect(getTraffic).to.include('const servingState = {}');
expect(getTraffic).to.include('if (servingState[key] === undefined)');
});
});
});