ninedeploy 0.16.8 → 0.16.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -164,6 +164,23 @@ ninedeploy status-pages publish|unpublish|rm <id>
164
164
  ninedeploy status-pages notice <id> "Maintenance at 22:00" # --clear removes it
165
165
  ```
166
166
 
167
+ ### 🐤 Canary Releases (0.16.9)
168
+ ```bash
169
+ ninedeploy canary show web [--json] # Configuration, and the running canary: step, share, 5xx %, p95
170
+ ninedeploy canary set web --steps 10,50 --max-p95 800 # Options not given keep their value; --max-p95 off stops judging it
171
+ ninedeploy canary off web # The next deploy switches all traffic at once
172
+ ninedeploy canary promote web [--deployment <id>] # Skip the remaining steps; stop one with `deploys cancel`
173
+ ```
174
+
175
+ ### 📈 Autoscaling (0.16.10)
176
+ ```bash
177
+ ninedeploy autoscale show web [--json] # Bounds, metric and target, containers serving, last value, desired count, last change
178
+ ninedeploy autoscale set web --max 4 --target 70 # Turn it on (cpu by default); --metric memory|rps, --min, --up-seconds, --down-seconds
179
+ ninedeploy autoscale set web --metric rps --target 200 # Options not given keep their value; rps needs traffic analytics
180
+ ninedeploy autoscale off web # The next deploy runs the fixed replicas again
181
+ ```
182
+ Turning it on applies fully at the next deploy, which creates the spare containers.
183
+
167
184
  ---
168
185
 
169
186
  ## 📄 License
@@ -0,0 +1,38 @@
1
+ import { type AutoscaleConfig, type AutoscaleConfigInput, type AutoscaleMetric } from '@ninedeploy/schemas';
2
+ import type { NineDeployClient } from '../client.js';
3
+ export interface AutoscaleSetOptions {
4
+ min?: string;
5
+ max?: string;
6
+ metric?: string;
7
+ target?: string;
8
+ upSeconds?: string;
9
+ downSeconds?: string;
10
+ json?: boolean;
11
+ }
12
+ /**
13
+ * The fields the user gave, as a partial autoscale configuration; null after
14
+ * printing why one is not valid. Ranges are left to the schema.
15
+ */
16
+ export declare function autoscaleOptions(opts: AutoscaleSetOptions): Partial<AutoscaleConfigInput> | null;
17
+ /**
18
+ * What `autoscale set` sends. A service that has a configuration keeps the
19
+ * values not given (the PATCH replaces the whole object). Without one, only
20
+ * the given values go and the server fills in the defaults; `--max` and
21
+ * `--target` have none, so they are named when missing.
22
+ */
23
+ export declare function autoscaleBody(current: AutoscaleConfig | null | undefined, given: Partial<AutoscaleConfigInput>): {
24
+ body: AutoscaleConfigInput;
25
+ } | {
26
+ missing: string[];
27
+ };
28
+ /** A metric value with its unit: `70%` for cpu and memory, `120 req/s` for rps. */
29
+ export declare function fmtMetric(metric: AutoscaleMetric, v: number): string;
30
+ /** `ninedeploy autoscale show <service> [--json]` */
31
+ export declare function autoscaleShow(client: NineDeployClient, ref: string, opts?: {
32
+ json?: boolean;
33
+ }): Promise<void>;
34
+ /** `ninedeploy autoscale set <service> [...]` */
35
+ export declare function autoscaleSet(client: NineDeployClient, ref: string, opts?: AutoscaleSetOptions): Promise<void>;
36
+ /** `ninedeploy autoscale off <service>` */
37
+ export declare function autoscaleOff(client: NineDeployClient, ref: string): Promise<void>;
38
+ //# sourceMappingURL=autoscale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoscale.d.ts","sourceRoot":"","sources":["../../src/commands/autoscale.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EAErB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAuBrD,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAsBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAqBhG;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,EAC3C,KAAK,EAAE,OAAO,CAAC,oBAAoB,CAAC,GACnC;IAAE,IAAI,EAAE,oBAAoB,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAKxD;AAOD,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEpE;AA2BD,qDAAqD;AACrD,wBAAsB,aAAa,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBvH;AAED,iDAAiD;AACjD,wBAAsB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BvH;AAED,2CAA2C;AAC3C,wBAAsB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASvF"}
@@ -0,0 +1,176 @@
1
+ import { AUTOSCALE_METRICS, autoscaleConfig, } from '@ninedeploy/schemas';
2
+ import { c, error, fmtTime, header, info, kv, spinner, success } from '../lib/format.js';
3
+ import { fmtSeconds } from './canary.js';
4
+ import { plain } from './sources.js';
5
+ import { resolveServiceRef } from './uptime.js';
6
+ /**
7
+ * 0.16.10 autoscaling:
8
+ *
9
+ * ninedeploy autoscale show <service> [--json]
10
+ * ninedeploy autoscale set <service> [--min N] [--max N] [--metric cpu|memory|rps] [--target X]
11
+ * [--up-seconds N] [--down-seconds N] [--json]
12
+ * ninedeploy autoscale off <service>
13
+ *
14
+ * `<service>` is an id, a slug or a name. Turning it on applies fully at the
15
+ * next deploy, which creates the spare containers.
16
+ */
17
+ const message = (err) => plain(err instanceof Error ? err.message : String(err));
18
+ /** Skip the spinner for `--json` so stdout stays parseable. */
19
+ const load = (json, msg, fn) => (json ? fn() : spinner(msg, fn));
20
+ /** Flag names for the schema fields, so a validation failure names what the user typed. */
21
+ const FLAG = {
22
+ minReplicas: '--min',
23
+ maxReplicas: '--max',
24
+ metric: '--metric',
25
+ target: '--target',
26
+ scaleUpSeconds: '--up-seconds',
27
+ scaleDownSeconds: '--down-seconds',
28
+ };
29
+ const NUMBERS = [
30
+ ['min', 'minReplicas'],
31
+ ['max', 'maxReplicas'],
32
+ ['target', 'target'],
33
+ ['upSeconds', 'scaleUpSeconds'],
34
+ ['downSeconds', 'scaleDownSeconds'],
35
+ ];
36
+ /**
37
+ * The fields the user gave, as a partial autoscale configuration; null after
38
+ * printing why one is not valid. Ranges are left to the schema.
39
+ */
40
+ export function autoscaleOptions(opts) {
41
+ const out = {};
42
+ for (const [opt, field] of NUMBERS) {
43
+ const raw = opts[opt];
44
+ if (typeof raw !== 'string')
45
+ continue;
46
+ const n = Number(raw);
47
+ if (raw.trim() === '' || !Number.isFinite(n)) {
48
+ error(`${FLAG[field]} must be a number`);
49
+ return null;
50
+ }
51
+ out[field] = n;
52
+ }
53
+ if (opts.metric !== undefined) {
54
+ const metric = opts.metric.trim().toLowerCase();
55
+ if (!AUTOSCALE_METRICS.includes(metric)) {
56
+ error(`--metric must be one of ${AUTOSCALE_METRICS.join(', ')}`);
57
+ return null;
58
+ }
59
+ out.metric = metric;
60
+ }
61
+ return out;
62
+ }
63
+ /**
64
+ * What `autoscale set` sends. A service that has a configuration keeps the
65
+ * values not given (the PATCH replaces the whole object). Without one, only
66
+ * the given values go and the server fills in the defaults; `--max` and
67
+ * `--target` have none, so they are named when missing.
68
+ */
69
+ export function autoscaleBody(current, given) {
70
+ if (current)
71
+ return { body: { ...current, ...given } };
72
+ const missing = ['maxReplicas', 'target'].filter((f) => given[f] === undefined).map((f) => FLAG[f]);
73
+ if (missing.length > 0)
74
+ return { missing };
75
+ return { body: given };
76
+ }
77
+ /** A zod failure as one line, each issue led by the flag it is about. */
78
+ function validationMessage(err) {
79
+ return err.issues.map((i) => `${FLAG[String(i.path[0])] ?? 'autoscale'}: ${i.message}`).join('; ');
80
+ }
81
+ /** A metric value with its unit: `70%` for cpu and memory, `120 req/s` for rps. */
82
+ export function fmtMetric(metric, v) {
83
+ return metric === 'rps' ? `${v} req/s` : `${v}%`;
84
+ }
85
+ const containers = (n) => `${n} container${n === 1 ? '' : 's'}`;
86
+ function printConfig(cfg) {
87
+ if (!cfg) {
88
+ kv('Autoscaling', c.gray('off: the service runs its fixed replicas'));
89
+ return;
90
+ }
91
+ kv('Bounds', `${cfg.minReplicas}–${cfg.maxReplicas} containers`);
92
+ kv('Metric', `${cfg.metric}, target ${fmtMetric(cfg.metric, cfg.target)} per container`);
93
+ kv('Scale up after', fmtSeconds(cfg.scaleUpSeconds));
94
+ kv('Scale down after', fmtSeconds(cfg.scaleDownSeconds));
95
+ }
96
+ function printStatus(st) {
97
+ console.log();
98
+ kv('Serving', containers(st.replicas));
99
+ if (!st.enabled)
100
+ return;
101
+ const metric = st.metric ?? 'cpu';
102
+ kv('Last value', st.value === null || st.value === undefined ? c.gray('not read yet') : `${fmtMetric(metric, st.value)} per container`);
103
+ kv('Desired', st.desired === null || st.desired === undefined ? null : containers(st.desired));
104
+ if (st.holding)
105
+ info(`Not scaling now: ${plain(st.holding)}.`);
106
+ const ev = st.lastEvent;
107
+ kv('Last change', ev ? `${ev.from} → ${ev.to} containers, ${plain(ev.reason)}, ${fmtTime(ev.at)}` : c.gray('none yet'));
108
+ }
109
+ /** `ninedeploy autoscale show <service> [--json]` */
110
+ export async function autoscaleShow(client, ref, opts = {}) {
111
+ try {
112
+ const id = await resolveServiceRef(client, ref);
113
+ if (id === null)
114
+ return;
115
+ const [svc, status] = await load(opts.json, 'Loading autoscaling', () => Promise.all([client.services.get(id), client.services.autoscale(id)]));
116
+ if (opts.json) {
117
+ console.log(JSON.stringify({ config: svc.autoscale ?? null, status }, null, 2));
118
+ return;
119
+ }
120
+ header(`Autoscaling: ${plain(svc.name)}`);
121
+ printConfig(svc.autoscale);
122
+ printStatus(status);
123
+ console.log();
124
+ }
125
+ catch (err) {
126
+ error(message(err));
127
+ }
128
+ }
129
+ /** `ninedeploy autoscale set <service> [...]` */
130
+ export async function autoscaleSet(client, ref, opts = {}) {
131
+ const given = autoscaleOptions(opts);
132
+ if (given === null)
133
+ return;
134
+ try {
135
+ const id = await resolveServiceRef(client, ref);
136
+ if (id === null)
137
+ return;
138
+ const svc = await client.services.get(id);
139
+ const built = autoscaleBody(svc.autoscale, given);
140
+ if ('missing' in built) {
141
+ error(`Autoscaling is off for this service: give ${built.missing.join(' and ')} to turn it on.`);
142
+ return;
143
+ }
144
+ const parsed = autoscaleConfig.safeParse(built.body);
145
+ if (!parsed.success) {
146
+ error(validationMessage(parsed.error));
147
+ return;
148
+ }
149
+ const body = built.body;
150
+ const updated = await load(opts.json, 'Saving the autoscaling configuration', () => client.services.update(id, { autoscale: body }));
151
+ if (opts.json) {
152
+ console.log(JSON.stringify(updated.autoscale ?? null, null, 2));
153
+ return;
154
+ }
155
+ success(`Autoscaling on for ${plain(updated.name)}.`);
156
+ printConfig(updated.autoscale);
157
+ info('It applies fully at the next deploy, which creates the spare containers.');
158
+ }
159
+ catch (err) {
160
+ error(message(err));
161
+ }
162
+ }
163
+ /** `ninedeploy autoscale off <service>` */
164
+ export async function autoscaleOff(client, ref) {
165
+ try {
166
+ const id = await resolveServiceRef(client, ref);
167
+ if (id === null)
168
+ return;
169
+ const updated = await spinner('Turning autoscaling off', () => client.services.update(id, { autoscale: null }));
170
+ success(`Autoscaling off for ${plain(updated.name)}. The containers serving now keep running until the next deploy, which runs the fixed replicas again.`);
171
+ }
172
+ catch (err) {
173
+ error(message(err));
174
+ }
175
+ }
176
+ //# sourceMappingURL=autoscale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoscale.js","sourceRoot":"","sources":["../../src/commands/autoscale.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { type CanaryConfig, type CanaryConfigInput } from '@ninedeploy/schemas';
2
+ import type { NineDeployClient } from '../client.js';
3
+ export interface CanarySetOptions {
4
+ steps?: string;
5
+ stepSeconds?: string;
6
+ maxErrorRate?: string;
7
+ maxP95?: string;
8
+ minRequests?: string;
9
+ json?: boolean;
10
+ }
11
+ /**
12
+ * The fields the user gave, as a partial canary configuration; null after
13
+ * printing why one is not valid. Ranges are left to the schema.
14
+ */
15
+ export declare function canaryOptions(opts: CanarySetOptions): Partial<CanaryConfigInput> | null;
16
+ /**
17
+ * What `canary set` sends. A service that has a configuration keeps the
18
+ * values not given (the PATCH replaces the whole object). Without one, only
19
+ * the given values go, and the server fills in the rest; `steps` has no
20
+ * server default, so the documented default (10, 50) is sent when not given.
21
+ */
22
+ export declare function canaryBody(current: CanaryConfig | null | undefined, given: Partial<CanaryConfigInput>): CanaryConfigInput;
23
+ /** Seconds as `1m 20s`. */
24
+ export declare function fmtSeconds(total: number): string;
25
+ /** `ninedeploy canary show <service> [--json]` */
26
+ export declare function canaryShow(client: NineDeployClient, ref: string, opts?: {
27
+ json?: boolean;
28
+ }): Promise<void>;
29
+ /** `ninedeploy canary set <service> [...]` */
30
+ export declare function canarySet(client: NineDeployClient, ref: string, opts?: CanarySetOptions): Promise<void>;
31
+ /** `ninedeploy canary off <service>` */
32
+ export declare function canaryOff(client: NineDeployClient, ref: string): Promise<void>;
33
+ /** `ninedeploy canary promote <service> [--deployment <id>]` */
34
+ export declare function canaryPromote(client: NineDeployClient, ref: string, opts?: {
35
+ deployment?: string;
36
+ }): Promise<void>;
37
+ //# sourceMappingURL=canary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canary.d.ts","sourceRoot":"","sources":["../../src/commands/canary.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAGvB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAwBrD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAqBD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAgCvF;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,iBAAiB,CAGzH;AAOD,2BAA2B;AAC3B,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhD;AAwCD,kDAAkD;AAClD,wBAAsB,UAAU,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpH;AAED,8CAA8C;AAC9C,wBAAsB,SAAS,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBjH;AAED,wCAAwC;AACxC,wBAAsB,SAAS,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASpF;AAED,gEAAgE;AAChE,wBAAsB,aAAa,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B5H"}
@@ -0,0 +1,230 @@
1
+ import { CANARY_DEFAULT_STEPS, canaryConfig, } from '@ninedeploy/schemas';
2
+ import { c, error, header, info, kv, spinner, success } from '../lib/format.js';
3
+ import { plain } from './sources.js';
4
+ import { resolveServiceRef } from './uptime.js';
5
+ /**
6
+ * 0.16.9 canary releases:
7
+ *
8
+ * ninedeploy canary show <service> [--json]
9
+ * ninedeploy canary set <service> [--steps 10,50] [--step-seconds 120] [--max-error-rate 5]
10
+ * [--max-p95 <ms>|off] [--min-requests 20] [--json]
11
+ * ninedeploy canary off <service>
12
+ * ninedeploy canary promote <service> [--deployment <id>]
13
+ *
14
+ * `<service>` is an id, a slug or a name. Stopping a running canary is
15
+ * `ninedeploy deploys cancel <serviceId> <deployId>`: all traffic goes back
16
+ * to the previous release.
17
+ */
18
+ const message = (err) => plain(err instanceof Error ? err.message : String(err));
19
+ /** Skip the spinner for `--json` so stdout stays parseable. */
20
+ const load = (json, msg, fn) => (json ? fn() : spinner(msg, fn));
21
+ /** Flag names for the schema fields, so a validation failure names what the user typed. */
22
+ const FLAG = {
23
+ steps: '--steps',
24
+ stepSeconds: '--step-seconds',
25
+ maxErrorRate: '--max-error-rate',
26
+ maxP95Ms: '--max-p95',
27
+ minRequests: '--min-requests',
28
+ };
29
+ /** A number option; null after printing why not. */
30
+ function numberOption(raw, flag) {
31
+ const n = Number(raw);
32
+ if (raw.trim() === '' || !Number.isFinite(n)) {
33
+ error(`${flag} must be a number`);
34
+ return null;
35
+ }
36
+ return n;
37
+ }
38
+ /**
39
+ * The fields the user gave, as a partial canary configuration; null after
40
+ * printing why one is not valid. Ranges are left to the schema.
41
+ */
42
+ export function canaryOptions(opts) {
43
+ const out = {};
44
+ if (opts.steps !== undefined) {
45
+ const parts = opts.steps.split(',').map((s) => s.trim());
46
+ const steps = parts.map(Number);
47
+ if (parts.some((p) => p === '') || steps.some((n) => !Number.isFinite(n))) {
48
+ error('--steps must be percentages separated by commas, for example 10,50');
49
+ return null;
50
+ }
51
+ out.steps = steps;
52
+ }
53
+ const numbers = [
54
+ ['stepSeconds', 'stepSeconds'],
55
+ ['maxErrorRate', 'maxErrorRate'],
56
+ ['minRequests', 'minRequests'],
57
+ ];
58
+ for (const [opt, field] of numbers) {
59
+ const raw = opts[opt];
60
+ if (typeof raw !== 'string')
61
+ continue;
62
+ const n = numberOption(raw, FLAG[field]);
63
+ if (n === null)
64
+ return null;
65
+ out[field] = n;
66
+ }
67
+ if (opts.maxP95 !== undefined) {
68
+ if (opts.maxP95.trim().toLowerCase() === 'off')
69
+ out.maxP95Ms = null;
70
+ else {
71
+ const n = numberOption(opts.maxP95, '--max-p95');
72
+ if (n === null)
73
+ return null;
74
+ out.maxP95Ms = n;
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+ /**
80
+ * What `canary set` sends. A service that has a configuration keeps the
81
+ * values not given (the PATCH replaces the whole object). Without one, only
82
+ * the given values go, and the server fills in the rest; `steps` has no
83
+ * server default, so the documented default (10, 50) is sent when not given.
84
+ */
85
+ export function canaryBody(current, given) {
86
+ if (current)
87
+ return { ...current, ...given };
88
+ return { ...given, steps: given.steps ?? [...CANARY_DEFAULT_STEPS] };
89
+ }
90
+ /** A zod failure as one line, each issue led by the flag it is about. */
91
+ function validationMessage(err) {
92
+ return err.issues.map((i) => `${FLAG[String(i.path[0])] ?? 'canary'}: ${i.message}`).join('; ');
93
+ }
94
+ /** Seconds as `1m 20s`. */
95
+ export function fmtSeconds(total) {
96
+ const s = Math.max(0, Math.round(total));
97
+ const m = Math.floor(s / 60);
98
+ return m > 0 ? `${m}m ${s % 60}s` : `${s}s`;
99
+ }
100
+ function printConfig(cfg) {
101
+ if (!cfg) {
102
+ kv('Canary', c.gray('off: a deploy switches all traffic at once'));
103
+ return;
104
+ }
105
+ kv('Steps', cfg.steps.map((s) => `${s}%`).join(', '));
106
+ kv('Step length', fmtSeconds(cfg.stepSeconds));
107
+ kv('Max 5xx rate', `${cfg.maxErrorRate}%`);
108
+ kv('Max p95', cfg.maxP95Ms === null ? c.gray('not judged') : `${cfg.maxP95Ms} ms`);
109
+ kv('Min requests', cfg.minRequests);
110
+ }
111
+ function stats(s) {
112
+ if (!s)
113
+ return c.gray('—');
114
+ const rate = s.errorRate === null ? '—' : `${s.errorRate}%`;
115
+ const p95 = s.p95Ms === null ? '—' : `${s.p95Ms} ms`;
116
+ return `${s.requests} requests, 5xx ${rate}, p95 ${p95}`;
117
+ }
118
+ function printStatus(st, now) {
119
+ console.log();
120
+ if (!st.active) {
121
+ info('No canary is running.');
122
+ return;
123
+ }
124
+ console.log(` ${c.bold('Running canary')}`);
125
+ kv('Deployment', `#${st.deploymentId}`);
126
+ kv('Step', `${st.step}/${st.steps}`);
127
+ kv('Traffic', `${st.weight}% to the new release`);
128
+ if (st.stepEndsAt)
129
+ kv('Step ends in', fmtSeconds((Date.parse(st.stepEndsAt) - now) / 1000));
130
+ kv('New release', stats(st.canary));
131
+ kv('Previous', stats(st.stable));
132
+ if (st.measured === false) {
133
+ info('Traffic analytics is off: steps are judged on the healthcheck only.');
134
+ }
135
+ if (st.promoting)
136
+ info('Promote requested: all traffic is switching to the new release.');
137
+ }
138
+ /** `ninedeploy canary show <service> [--json]` */
139
+ export async function canaryShow(client, ref, opts = {}) {
140
+ try {
141
+ const id = await resolveServiceRef(client, ref);
142
+ if (id === null)
143
+ return;
144
+ const [svc, status] = await load(opts.json, 'Loading the canary', () => Promise.all([client.services.get(id), client.deploys.canary(id)]));
145
+ if (opts.json) {
146
+ console.log(JSON.stringify({ config: svc.canary ?? null, status }, null, 2));
147
+ return;
148
+ }
149
+ header(`Canary releases: ${plain(svc.name)}`);
150
+ printConfig(svc.canary);
151
+ printStatus(status, Date.now());
152
+ console.log();
153
+ }
154
+ catch (err) {
155
+ error(message(err));
156
+ }
157
+ }
158
+ /** `ninedeploy canary set <service> [...]` */
159
+ export async function canarySet(client, ref, opts = {}) {
160
+ const given = canaryOptions(opts);
161
+ if (given === null)
162
+ return;
163
+ try {
164
+ const id = await resolveServiceRef(client, ref);
165
+ if (id === null)
166
+ return;
167
+ const svc = await client.services.get(id);
168
+ const body = canaryBody(svc.canary, given);
169
+ const parsed = canaryConfig.safeParse(body);
170
+ if (!parsed.success) {
171
+ error(validationMessage(parsed.error));
172
+ return;
173
+ }
174
+ const updated = await load(opts.json, 'Saving the canary configuration', () => client.services.update(id, { canary: body }));
175
+ if (opts.json) {
176
+ console.log(JSON.stringify(updated.canary ?? null, null, 2));
177
+ return;
178
+ }
179
+ success(`Canary releases on for ${plain(updated.name)}. The next deploy uses them.`);
180
+ printConfig(updated.canary);
181
+ }
182
+ catch (err) {
183
+ error(message(err));
184
+ }
185
+ }
186
+ /** `ninedeploy canary off <service>` */
187
+ export async function canaryOff(client, ref) {
188
+ try {
189
+ const id = await resolveServiceRef(client, ref);
190
+ if (id === null)
191
+ return;
192
+ const updated = await spinner('Turning canary releases off', () => client.services.update(id, { canary: null }));
193
+ success(`Canary releases off for ${plain(updated.name)}. A deploy switches all traffic at once.`);
194
+ }
195
+ catch (err) {
196
+ error(message(err));
197
+ }
198
+ }
199
+ /** `ninedeploy canary promote <service> [--deployment <id>]` */
200
+ export async function canaryPromote(client, ref, opts = {}) {
201
+ let depId;
202
+ if (opts.deployment !== undefined) {
203
+ const n = Number(opts.deployment);
204
+ if (!Number.isInteger(n) || n <= 0) {
205
+ error('--deployment must be a positive integer');
206
+ return;
207
+ }
208
+ depId = n;
209
+ }
210
+ try {
211
+ const id = await resolveServiceRef(client, ref);
212
+ if (id === null)
213
+ return;
214
+ if (depId === undefined) {
215
+ const status = await client.deploys.canary(id);
216
+ if (!status.active || status.deploymentId === undefined) {
217
+ error('No canary is running on this service.');
218
+ return;
219
+ }
220
+ depId = status.deploymentId;
221
+ }
222
+ const target = depId;
223
+ await spinner('Promoting the canary', () => client.deploys.promoteCanary(id, target));
224
+ success(`Deployment #${target}: all traffic is switching to the new release.`);
225
+ }
226
+ catch (err) {
227
+ error(message(err));
228
+ }
229
+ }
230
+ //# sourceMappingURL=canary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canary.js","sourceRoot":"","sources":["../../src/commands/canary.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -48,6 +48,8 @@ import { panelBackupDecryptAction, panelBackupListAction, panelBackupNowAction,
48
48
  import { serversList, serversRoles, servicesPlacement, servicesSwarm, servicesTransfers, sourcesAllowOnNodes, sourcesHostKeys, sourcesPinHostKeys, sourcesResetHostKeys, swarmInit, swarmJoin, swarmLeave, swarmSetEnabled, swarmStatus, volumesCreate, } from './commands/multiNode.js';
49
49
  // ── end 0.16 T8 ──
50
50
  import { monitorsAdd, monitorsCheck, monitorsList, monitorsRemove, monitorsSetEnabled, monitorsShow, statusPagesAdd, statusPagesList, statusPagesNotice, statusPagesRemove, statusPagesSetPublished, } from './commands/uptime.js';
51
+ import { canaryOff, canaryPromote, canarySet, canaryShow } from './commands/canary.js';
52
+ import { autoscaleOff, autoscaleSet, autoscaleShow } from './commands/autoscale.js';
51
53
  const program = new Command();
52
54
  program
53
55
  .name('ninedeploy')
@@ -988,6 +990,47 @@ statusPagesCmd.command('rm <id>')
988
990
  .option('-y, --yes', 'Skip the confirmation prompt')
989
991
  .action((id, opts) => statusPagesRemove(getClient(), id, opts));
990
992
  /* v8 ignore stop */
993
+ // ── 0.16.9: canary releases ──────────────────────────────────────────────
994
+ const canaryCmd = program.command('canary').description('Canary releases: shift traffic to a new release in steps');
995
+ /* v8 ignore start -- exercised by test/canary.test.ts. */
996
+ canaryCmd.command('show <service>')
997
+ .description('The canary configuration and the canary running now')
998
+ .option('--json', 'Print raw JSON')
999
+ .action((service, opts) => canaryShow(getClient(), service, opts));
1000
+ canaryCmd.command('set <service>')
1001
+ .description('Turn canary releases on or change them (docker services); options not given keep their value')
1002
+ .option('--steps <list>', 'Percent of requests per step, rising, 1-99, up to 5 (default 10,50)')
1003
+ .option('--step-seconds <n>', 'Seconds per step (30-3600, default 120)')
1004
+ .option('--max-error-rate <percent>', 'Highest share of 5xx responses the new release may have (default 5)')
1005
+ .option('--max-p95 <ms>', 'Highest p95 response time in ms, or off (default off)')
1006
+ .option('--min-requests <n>', 'Fewer requests in a step are not judged (default 20)')
1007
+ .option('--json', 'Print the stored configuration as JSON')
1008
+ .action((service, opts) => canarySet(getClient(), service, opts));
1009
+ canaryCmd.command('off <service>').description('Turn canary releases off').action((service) => canaryOff(getClient(), service));
1010
+ canaryCmd.command('promote <service>')
1011
+ .description('Skip the remaining steps and send all traffic to the new release now')
1012
+ .option('--deployment <id>', 'The deployment to promote (default: the one running)')
1013
+ .action((service, opts) => canaryPromote(getClient(), service, opts));
1014
+ /* v8 ignore stop */
1015
+ // ── 0.16.10: autoscaling ─────────────────────────────────────────────────
1016
+ const autoscaleCmd = program.command('autoscale').description('Autoscaling: run between a minimum and a maximum number of containers, set by load');
1017
+ /* v8 ignore start -- exercised by test/autoscale.test.ts. */
1018
+ autoscaleCmd.command('show <service>')
1019
+ .description('The autoscaling configuration and what it is doing now')
1020
+ .option('--json', 'Print raw JSON')
1021
+ .action((service, opts) => autoscaleShow(getClient(), service, opts));
1022
+ autoscaleCmd.command('set <service>')
1023
+ .description('Turn autoscaling on or change it (docker services); options not given keep their value')
1024
+ .option('--min <n>', 'Containers it never goes below (1-10, default 1)')
1025
+ .option('--max <n>', 'Containers it never goes above (2-10, above --min; required to turn it on)')
1026
+ .option('--metric <metric>', 'cpu, memory or rps (default cpu)')
1027
+ .option('--target <value>', 'Value per container to keep near: percent for cpu/memory, requests per second for rps (required to turn it on)')
1028
+ .option('--up-seconds <n>', 'How long the need must last before adding containers (30-3600, default 60)')
1029
+ .option('--down-seconds <n>', 'How long the need must last before removing containers (60-3600, default 300)')
1030
+ .option('--json', 'Print the stored configuration as JSON')
1031
+ .action((service, opts) => autoscaleSet(getClient(), service, opts));
1032
+ autoscaleCmd.command('off <service>').description('Turn autoscaling off').action((service) => autoscaleOff(getClient(), service));
1033
+ /* v8 ignore stop */
991
1034
  deploys.command('watch <serviceId> <deployId>').description('Stream a deployment\'s build logs live').action((svcId, depId) => deploysWatch(svcId, depId));
992
1035
  // ── Diagnostics ───────────────────────────────────────────────────────────
993
1036
  program