Skip to content

Commit 2be605e

Browse files
authored
Major 2.x.x (#88)
1 parent 424ddf4 commit 2be605e

13 files changed

Lines changed: 86 additions & 92 deletions

README.md

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,13 @@ Deploy application to docker swarm in a controlled manner.
33

44
# Why?
55

6-
- Rolling update of swarm config, thanks to checksum naming.
7-
- Explicit syntax, no more optionals, no more short syntax.
8-
- Built-in jinja2 style templating via nunjucks
6+
- Rolling update of swarm config, thanks to checksum naming
7+
- Explicit syntax, no more optionals, no more short syntax
8+
- [Built-in jinja2 style templating via nunjucks](./examples/swarm-app.yml?plain=1L13)
9+
- [Inline swarm configs with envsubst](./examples/swarm-app.yml?plain=1L34)
910

1011
# Usage
1112
- `swarm-app validate` will exit on basic configuration file mistakes.
1213
- `swarm-app diff` gives a proper diff overview of what you are about to deploy.
1314
- `swarm-app deploy` deploys the application.
1415
- `swarm-app wait` waits for deployment to reconcile, and outputs status.
15-
16-
## Inline swarm configs with envsubst
17-
```sh
18-
export NGINX_FOLDER=html
19-
```
20-
21-
```yml
22-
services:
23-
nginx:
24-
configs:
25-
/etc/nginx/conf.d/default.conf:
26-
content: |
27-
server {
28-
location / {
29-
root ${NGINX_FOLDER};
30-
}
31-
}
32-
```

examples/deploy.sh

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
set -e
44

5-
export STACK_NAME="test";
6-
75
docker network inspect external &>/dev/null || docker network create external --driver=overlay
86

9-
NGINX_FOLDER="/usr/share/nginx/html" \
10-
node ../src/index.js deploy "$STACK_NAME" -f swarm-app.yml -i swarm-app.input.yml
7+
export STACK_NAME="test"
8+
export NGINX_FOLDER="/usr/share/nginx/html"
9+
export NGINX_LOCATION="/public"
10+
11+
node ../src/index.js deploy "$STACK_NAME" -f swarm-app.yml -i swarm-app.input.yml
1112

1213
node ../src/index.js wait "$STACK_NAME"

examples/diff.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
set -e
44

5-
export STACK_NAME="test";
5+
export STACK_NAME="test"
6+
export NGINX_FOLDER="/usr/share/nginx/html"
7+
export NGINX_LOCATION="/public"
68

79
node ../src/index.js diff --write-lhs-rhs -f swarm-app.yml -f swarm-app.diff.yml -i swarm-app.input.yml "$STACK_NAME"

examples/swarm-app.diff.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
services:
2+
service_specs:
33

44
nginx:
55
image: nginx:alpine

examples/swarm-app.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ networks:
88
name: external
99
external: true
1010

11-
services:
11+
service_specs:
1212

1313
# {% if echo_servers is defined %}
1414
# {% for entry in echo_servers %}
@@ -27,7 +27,7 @@ services:
2727
org.company.country: england
2828
configs:
2929
/etc/nginx/nginx.conf:
30-
sourceFile: 'nginx.conf'
30+
source_file: 'nginx.conf'
3131
/etc/nginx/conf.d/default.conf:
3232
content: |
3333
server {
@@ -49,15 +49,17 @@ services:
4949
stop_signal: SIGQUIT
5050
stop_grace_period: 10
5151
placement:
52-
preferences: [{ spread: node.hostname }]
52+
preferences:
53+
- { spread: node.hostname }
5354
max_replicas_per_node: 2
5455
constraints:
5556
- node.labels.purpose == generic
5657
endpoint_spec:
5758
ports:
5859
- protocol: tcp
59-
published: 8080
60-
target: 80
60+
published_port: 8080
61+
target_port: 80
62+
publish_mode: host
6163
health_check:
6264
test: ["CMD", "true"]
6365
interval: 5000000 # 5s

schema.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "http://json-schema.org/draft-07/schema#",
33
"title": "Generated schema for swarm-app",
44
"properties": {
5-
"services": {
5+
"service_specs": {
66
"values": {
77
"optionalProperties": {
88
"extends": {
@@ -43,7 +43,7 @@
4343
"configs": {
4444
"values": {
4545
"optionalProperties": {
46-
"sourceFile": {
46+
"source_file": {
4747
"type": "string"
4848
},
4949
"content": {
@@ -103,14 +103,20 @@
103103
"ports": {
104104
"elements": {
105105
"properties": {
106-
"published": {
106+
"published_port": {
107107
"type": "int16"
108108
},
109-
"target": {
109+
"target_port": {
110110
"type": "int16"
111111
}
112112
},
113113
"optionalProperties": {
114+
"publish_mode": {
115+
"enum": [
116+
"ingress",
117+
"host"
118+
]
119+
},
114120
"protocol": {
115121
"enum": [
116122
"tcp",

src/asserts.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import assert from "assert";
22

3-
export function assertNotNullOrUndefined<T> (value: T | null | undefined, msg: string): asserts value is T {
4-
assert(value == null, msg);
5-
}
6-
73
export function assertString (value: unknown, msg: string): asserts value is string {
84
assert(typeof value === "string", msg);
95
}

src/commands/diff-cmd.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ interface InitServiceResourcesOpt {
5050
}
5151
function initServiceResources ({appName, config, hashedConfigs, current}: InitServiceResourcesOpt): ServiceSpec[] {
5252
const serviceSpecs: ServiceSpec[] = [];
53-
for (const serviceName of Object.keys(config.services)) {
53+
for (const serviceName of Object.keys(config.service_specs)) {
5454
const serviceSpec = initServiceSpec({appName, serviceName, config, hashedConfigs, current});
5555
delete serviceSpec.version;
5656
serviceSpecs.push(serviceSpec);

src/commands/wait-cmd.ts

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import {ArgumentsCamelCase, Argv} from "yargs";
2-
import Docker from "dockerode";
2+
import Docker, {Service} from "dockerode";
33
import timers from "timers/promises";
44
import {assertNumber, assertString} from "../asserts.js";
55
import {yargsAppNameFileOption} from "./deploy-cmd";
6+
import assert from "assert";
67

78
interface Task {
9+
ID: string;
10+
ServiceID: string;
11+
Slot: number;
812
DesiredState: string;
913
Status: {
1014
State: string;
@@ -23,50 +27,49 @@ export async function handler (args: ArgumentsCamelCase) {
2327

2428
const dockerode = new Docker();
2529

26-
console.log(`Awaiting task reconciliation for a max of ${timeout}ms`);
30+
console.log(`Awaiting task reconciliation for ${timeout}ms`);
2731

28-
let services;
29-
let timedout = false;
30-
let reconciled;
31-
let latestTaskError = "";
32+
let services: Service[], tasks: Task[], timedout, bail, serviceStateMap;
3233
const start = Date.now();
3334
do {
34-
latestTaskError = "";
35-
reconciled = true;
35+
serviceStateMap = new Map<string, string>();
3636
services = await dockerode.listServices({filters: {label: [`com.docker.stack.namespace=${appName}`]}});
37+
tasks = await dockerode.listTasks({filters: {"label": [`com.docker.stack.namespace=${appName}`], "desired-state": ["running"]}}) as Task[];
3738

38-
// Check the tasks for failures.
3939
for (const s of services) {
40-
const tasks = await dockerode.listTasks({
41-
Filter: `service=${s.Spec?.Name}`,
42-
}) as Task[];
43-
for (const t of tasks) {
44-
if (t.DesiredState === "ready" && t.Status.State != "running") {
45-
reconciled = false;
46-
}
47-
if (t.Status.State === "rejected" && latestTaskError == "") {
48-
latestTaskError = t.Status.Err;
40+
if (s.UpdateStatus?.State) {
41+
serviceStateMap.set(s.ID, s.UpdateStatus.State);
42+
} else {
43+
const runningTasks = tasks.filter(t => t.Status.State === "running" && t.ServiceID === s.ID);
44+
const totalTasks = tasks.filter(t => t.ServiceID === s.ID);
45+
if (totalTasks.length > runningTasks.length) {
46+
serviceStateMap.set(s.ID ?? "unspecified", "replicating");
4947
}
5048
}
5149
}
5250

51+
const servicesUpdating = [...serviceStateMap].filter(([v]) => !["completed", "rollback_completed"].includes(v));
52+
bail = servicesUpdating.length === 0;
53+
if (!bail) {
54+
servicesUpdating.forEach(([serviceId, state]) => {
55+
const serviceName = services.find(s => s.ID === serviceId)?.Spec?.Name;
56+
assert(serviceName != null, "serviceName must be a string");
57+
const errMsg = tasks.find(t => t.ServiceID === serviceId && t.Status.Err)?.Status.Err;
58+
console.log(`${serviceName} is in ${state}${errMsg ? ", error: '" + errMsg + "'" : ""}`);
59+
});
60+
}
61+
5362
// To prevent high cpu usage
5463
await timers.setTimeout(5000);
64+
// Calculate timedout
5565
timedout = Date.now() - timeout > start;
56-
if (!reconciled && latestTaskError != "") {
57-
console.error(latestTaskError);
58-
}
59-
} while (!timedout && !reconciled);
60-
61-
if (!reconciled || timedout) {
62-
if (timedout) {
63-
console.error("Reconciliation timed out");
64-
} else {
65-
console.error("Reconciliation failed");
66-
}
66+
} while (!timedout && !bail);
6767

68+
if (timedout) {
69+
console.error("Reconciliation timed out");
6870
process.exit(1);
6971
}
72+
7073
console.log("Reconciliation succeeded");
7174
}
7275

src/docker-api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export async function createMissingNetworks ({dockerode, current, config, appNam
7070

7171
const listNetworks = await dockerode.listNetworks({filters: {label: [`com.docker.stack.namespace=${appName}`]}});
7272
foundNetwork = listNetworks.find((ln) => ln.Name === n.name) as NetworkInspectInfoPlus | undefined;
73-
assert(foundNetwork != null, `Network ${n.name} could not be found, it has just have been created!`);
73+
assert(foundNetwork != null, `Network ${n.name} could not be found, it should have just have been created!`);
7474
newNetworks.push(foundNetwork);
7575
}
7676
return newNetworks;
@@ -101,7 +101,7 @@ export async function removeUnusedServices ({dockerode, current, config, appName
101101
for (const s of current.services) {
102102
if (!s.Spec?.Name) continue;
103103
const serviceShortName = s.Spec.Name.replace(new RegExp(`^${appName}_`), "");
104-
if (config.services[serviceShortName]) continue;
104+
if (config.service_specs[serviceShortName]) continue;
105105
console.log(`Removing service ${s.Spec.Name}`);
106106
await dockerode.getService(s.ID).remove();
107107
}
@@ -115,7 +115,7 @@ interface UpsertServicesOpts {
115115
hashedConfigs: HashedConfigs;
116116
}
117117
export async function upsertServices ({dockerode, config, current, appName, hashedConfigs}: UpsertServicesOpts) {
118-
for (const serviceName of Object.keys(config.services)) {
118+
for (const serviceName of Object.keys(config.service_specs)) {
119119
const serviceSpec = initServiceSpec({appName, serviceName, config, hashedConfigs, current});
120120
const foundService = current.services.find((s) => s.Spec?.Name === `${appName}_${serviceName}`);
121121
if (!foundService) {

0 commit comments

Comments
 (0)