|
1 | 1 | import {ArgumentsCamelCase, Argv} from "yargs"; |
2 | 2 | import Docker from "dockerode"; |
3 | 3 | import timers from "timers/promises"; |
4 | | -import {assertString} from "../asserts.js"; |
| 4 | +import {assertNumber, assertString} from "../asserts.js"; |
| 5 | + |
| 6 | +interface Task { |
| 7 | + DesiredState: string; |
| 8 | + Status: { |
| 9 | + State: string; |
| 10 | + Err: string; |
| 11 | + }; |
| 12 | +} |
5 | 13 |
|
6 | 14 | export const command = "wait <app-name>"; |
7 | 15 | export const description = "Wait for deployment to settle"; |
8 | 16 |
|
9 | 17 | export async function handler (args: ArgumentsCamelCase) { |
10 | 18 | const appName = args["appName"]; |
11 | 19 | assertString(appName); |
| 20 | + const timeout = args["timeout"]; |
| 21 | + assertNumber(timeout); |
12 | 22 |
|
13 | 23 | const dockerode = new Docker(); |
14 | 24 |
|
15 | | - console.log("Waiting for deployment reconciliation"); |
| 25 | + console.log(`Awaiting task reconciliation for a max of ${timeout}ms`); |
16 | 26 |
|
17 | 27 | let services; |
18 | | - let reconciled = false; |
| 28 | + let timedout = false; |
| 29 | + let reconciled; |
| 30 | + let latestTaskError = ""; |
| 31 | + const start = Date.now(); |
19 | 32 | do { |
| 33 | + latestTaskError = ""; |
| 34 | + reconciled = true; |
20 | 35 | services = await dockerode.listServices({filters: {label: [`com.docker.stack.namespace=${appName}`]}}); |
21 | | - reconciled = services.every(s => { |
22 | | - if (!s.UpdateStatus?.State) return true; |
23 | | - return ["completed", "rollback_completed", "paused", "rollback_paused"].includes(s.UpdateStatus?.State ?? ""); |
24 | | - }); |
25 | | - await timers.setTimeout(1000); |
26 | | - } while (!reconciled); |
27 | | - |
28 | | - const paused = services.filter(s => { |
29 | | - return ["paused", "rollback_paused"].includes(s.UpdateStatus?.State ?? ""); |
30 | | - }); |
31 | | - if (paused.length > 0) { |
32 | | - for (const s of paused) { |
33 | | - console.error(`${s.Spec?.Name} ${s.UpdateStatus?.Message}`); |
| 36 | + |
| 37 | + // Check the tasks for failures. |
| 38 | + for (const s of services) { |
| 39 | + const tasks = await dockerode.listTasks({ |
| 40 | + Filter: `service=${s.Spec?.Name}`, |
| 41 | + }) as Task[]; |
| 42 | + for (const t of tasks) { |
| 43 | + if (t.DesiredState === "ready" && t.Status.State != "running") { |
| 44 | + reconciled = false; |
| 45 | + } |
| 46 | + if (t.Status.State === "rejected" && latestTaskError == "") { |
| 47 | + latestTaskError = t.Status.Err; |
| 48 | + } |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + // To prevent high cpu usage |
| 53 | + await timers.setTimeout(5000); |
| 54 | + timedout = Date.now() - timeout > start; |
| 55 | + if (!reconciled && latestTaskError != "") { |
| 56 | + console.error(latestTaskError); |
| 57 | + } |
| 58 | + } while (!timedout && !reconciled); |
| 59 | + |
| 60 | + if (!reconciled || timedout) { |
| 61 | + if (timedout) { |
| 62 | + console.error("Reconciliation timed out"); |
| 63 | + } else { |
| 64 | + console.error("Reconciliation failed"); |
34 | 65 | } |
35 | | - console.error("Reconciliation failed"); |
36 | | - } else { |
37 | | - console.log("Reconciliation succeeded"); |
| 66 | + |
| 67 | + process.exit(1); |
38 | 68 | } |
| 69 | + console.log("Reconciliation succeeded"); |
39 | 70 | } |
40 | 71 |
|
41 | 72 | export function builder (yargs: Argv) { |
42 | 73 | yargs.positional("app-name", { |
43 | 74 | type: "string", |
44 | 75 | description: "Application name", |
45 | 76 | }); |
| 77 | + yargs.positional("timeout", { |
| 78 | + type: "number", |
| 79 | + description: "Time is ms to wait for reconciliation", |
| 80 | + default: 120000, |
| 81 | + }); |
46 | 82 | yargs.hide("help"); |
47 | 83 | yargs.hide("version"); |
48 | 84 | return yargs; |
|
0 commit comments