@fabioplunser/epd 0.1.0 → 0.1.1
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/dist/cli.js +228 -228
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1484,9 +1484,9 @@ function declFor(cfg) {
|
|
|
1484
1484
|
extra: cfg.proxy.extra
|
|
1485
1485
|
};
|
|
1486
1486
|
}
|
|
1487
|
-
async function readDecls(
|
|
1487
|
+
async function readDecls(host, cfg) {
|
|
1488
1488
|
const dir = paths(cfg).proxyAppsDir;
|
|
1489
|
-
const out = await
|
|
1489
|
+
const out = await host.capture(`for f in ${shq(dir)}/*.json; do [ -f "$f" ] || continue; echo "###EPD_FILE"; cat "$f"; done`, { allowFailure: true });
|
|
1490
1490
|
const decls = [];
|
|
1491
1491
|
for (const chunk of out.split("###EPD_FILE")) {
|
|
1492
1492
|
const t = chunk.trim();
|
|
@@ -1495,7 +1495,7 @@ async function readDecls(host2, cfg) {
|
|
|
1495
1495
|
try {
|
|
1496
1496
|
decls.push(JSON.parse(t));
|
|
1497
1497
|
} catch {
|
|
1498
|
-
log.warn(`${
|
|
1498
|
+
log.warn(`${host.name}: ignoring an unreadable proxy declaration`);
|
|
1499
1499
|
}
|
|
1500
1500
|
}
|
|
1501
1501
|
return decls;
|
|
@@ -1647,18 +1647,18 @@ function dynamicConfig(cfg, endpoints, accessories = []) {
|
|
|
1647
1647
|
middlewares[key] = {
|
|
1648
1648
|
redirectRegex: { regex: "^https?://[^/]+(.*)", replacement: `${route.redirect.to.replace(/\/$/, "")}$1`, permanent: route.redirect.permanent }
|
|
1649
1649
|
};
|
|
1650
|
-
const
|
|
1651
|
-
ensureService(services,
|
|
1650
|
+
const svcKey = `${owner.key}-${route.port}`;
|
|
1651
|
+
ensureService(services, svcKey, owner.urlsFor(route.port), route, owner.health);
|
|
1652
1652
|
routers[routerKey] = {
|
|
1653
1653
|
rule,
|
|
1654
1654
|
entryPoints: [entrypoint],
|
|
1655
1655
|
middlewares: [...mws, key],
|
|
1656
|
-
service:
|
|
1656
|
+
service: svcKey,
|
|
1657
1657
|
...route.ssl ? { tls: tlsFor(route) } : {},
|
|
1658
1658
|
...route.priority !== undefined ? { priority: route.priority } : {}
|
|
1659
1659
|
};
|
|
1660
1660
|
if (route.ssl && cfg.proxy.httpsRedirect) {
|
|
1661
|
-
routers[`${routerKey}-http`] = { rule, entryPoints: ["web"], middlewares: [key], service:
|
|
1661
|
+
routers[`${routerKey}-http`] = { rule, entryPoints: ["web"], middlewares: [key], service: svcKey };
|
|
1662
1662
|
}
|
|
1663
1663
|
return;
|
|
1664
1664
|
}
|
|
@@ -1743,18 +1743,18 @@ function containerSpec(d, cfg, staticHash) {
|
|
|
1743
1743
|
args.push(d.image);
|
|
1744
1744
|
return args;
|
|
1745
1745
|
}
|
|
1746
|
-
async function ensureProxy(
|
|
1746
|
+
async function ensureProxy(host, cfg, opts = {}) {
|
|
1747
1747
|
if (!cfg.proxy.enabled)
|
|
1748
1748
|
return;
|
|
1749
1749
|
const p = paths(cfg);
|
|
1750
1750
|
const containerMode = cfg.mode === "docker";
|
|
1751
|
-
await
|
|
1752
|
-
const merged = mergeDecls(await readDecls(
|
|
1751
|
+
await host.writeFile(p.proxyApp, JSON.stringify(declFor(cfg), null, 2), "644");
|
|
1752
|
+
const merged = mergeDecls(await readDecls(host, cfg));
|
|
1753
1753
|
if (!Object.keys(merged.entrypoints).length)
|
|
1754
1754
|
merged.entrypoints = cfg.proxy.entrypoints;
|
|
1755
1755
|
const where = containerMode ? CONTAINER_PATHS : { dynamicDir: p.proxyDynamicDir, acmeFile: `${p.acmeDir}/acme.json` };
|
|
1756
1756
|
const staticJson = JSON.stringify(staticConfig(merged, where), null, 2);
|
|
1757
|
-
await
|
|
1757
|
+
await host.writeFile(p.proxyStatic, staticJson, "644");
|
|
1758
1758
|
if (merged.dnsEnv.length) {
|
|
1759
1759
|
const lines = merged.dnsEnv.map((name) => {
|
|
1760
1760
|
const value = opts.secrets?.[name];
|
|
@@ -1763,7 +1763,7 @@ async function ensureProxy(host2, cfg, opts = {}) {
|
|
|
1763
1763
|
}
|
|
1764
1764
|
return `${name}=${value}`;
|
|
1765
1765
|
});
|
|
1766
|
-
await
|
|
1766
|
+
await host.writeFile(p.proxyEnv, lines.join(`
|
|
1767
1767
|
`), "600");
|
|
1768
1768
|
}
|
|
1769
1769
|
if (merged.dashboard && merged.dashboardHost) {
|
|
@@ -1779,23 +1779,23 @@ async function ensureProxy(host2, cfg, opts = {}) {
|
|
|
1779
1779
|
}
|
|
1780
1780
|
}
|
|
1781
1781
|
};
|
|
1782
|
-
await
|
|
1782
|
+
await host.writeFile(`${p.proxyDynamicDir}/_dashboard.yml`, JSON.stringify(dash, null, 2), "644");
|
|
1783
1783
|
}
|
|
1784
1784
|
const specHash = hash32(staticJson + JSON.stringify(merged)).toString(36);
|
|
1785
1785
|
if (containerMode)
|
|
1786
|
-
await ensureProxyContainer(
|
|
1786
|
+
await ensureProxyContainer(host, cfg, merged, specHash, opts);
|
|
1787
1787
|
else
|
|
1788
|
-
await ensureProxyProcess(
|
|
1788
|
+
await ensureProxyProcess(host, cfg, merged, specHash, opts);
|
|
1789
1789
|
}
|
|
1790
|
-
async function ensureProxyContainer(
|
|
1790
|
+
async function ensureProxyContainer(host, cfg, merged, specHash, opts) {
|
|
1791
1791
|
const spec = containerSpec(merged, cfg, specHash);
|
|
1792
|
-
const state = await
|
|
1792
|
+
const state = await host.capture(`docker inspect -f '{{.State.Running}} {{index .Config.Labels "epd.spec"}}' ${PROXY_CONTAINER} 2>/dev/null || echo "missing"`);
|
|
1793
1793
|
const [running, currentHash] = state.split(/\s+/);
|
|
1794
1794
|
if (!opts.force && running === "true" && currentHash === specHash) {
|
|
1795
|
-
log.host(
|
|
1795
|
+
log.host(host.name, "proxy is up to date");
|
|
1796
1796
|
return;
|
|
1797
1797
|
}
|
|
1798
|
-
await
|
|
1798
|
+
await host.exec(`docker network inspect ${shq(merged.network)} >/dev/null 2>&1 || docker network create ${shq(merged.network)} >/dev/null
|
|
1799
1799
|
docker rm -f ${PROXY_CONTAINER} >/dev/null 2>&1 || true
|
|
1800
1800
|
docker pull ${shq(merged.image)} >/dev/null
|
|
1801
1801
|
${spec.map((a) => shq(a)).join(" ")} >/dev/null
|
|
@@ -1804,15 +1804,15 @@ docker inspect -f '{{.State.Running}}' ${PROXY_CONTAINER} | grep -q true || {
|
|
|
1804
1804
|
echo "traefik failed to start:" >&2
|
|
1805
1805
|
docker logs --tail 40 ${PROXY_CONTAINER} >&2 || true
|
|
1806
1806
|
exit 1
|
|
1807
|
-
}`, { label: `${
|
|
1808
|
-
log.host(
|
|
1807
|
+
}`, { label: `${host.name}: start proxy` });
|
|
1808
|
+
log.host(host.name, `proxy running (${entrypointSummary(merged)})`);
|
|
1809
1809
|
}
|
|
1810
1810
|
var TRAEFIK_FALLBACK_VERSION = "v3.3.7";
|
|
1811
|
-
async function ensureProxyProcess(
|
|
1811
|
+
async function ensureProxyProcess(host, cfg, merged, specHash, opts) {
|
|
1812
1812
|
const p = paths(cfg);
|
|
1813
1813
|
const wanted = merged.version ?? (/^traefik:(v\d+\.\d+\.\d+)$/.exec(merged.image)?.[1] ?? "");
|
|
1814
1814
|
const needsPrivilegedPort = Object.values(merged.entrypoints).some((port) => port < 1024);
|
|
1815
|
-
await
|
|
1815
|
+
await host.exec(`mkdir -p ${shq(`${p.proxyDir}/bin`)} ${shq(p.proxyLogDir)}
|
|
1816
1816
|
want=${shq(wanted)}
|
|
1817
1817
|
if [ -z "$want" ]; then
|
|
1818
1818
|
# Piping curl straight into grep -m1 closes the pipe early, which pipefail
|
|
@@ -1845,7 +1845,7 @@ if [ "$have" != "$want" ]; then
|
|
|
1845
1845
|
rm -rf "$tmp"
|
|
1846
1846
|
fi
|
|
1847
1847
|
${needsPrivilegedPort ? `command -v setcap >/dev/null 2>&1 && setcap 'cap_net_bind_service=+ep' ${shq(p.proxyBin)} 2>/dev/null || echo "note: could not grant the traefik binary permission to bind ports below 1024" >&2` : ""}
|
|
1848
|
-
true`, { stream: true, label: `${
|
|
1848
|
+
true`, { stream: true, label: `${host.name}: install traefik` });
|
|
1849
1849
|
const ecosystem = {
|
|
1850
1850
|
apps: [
|
|
1851
1851
|
{
|
|
@@ -1865,17 +1865,17 @@ true`, { stream: true, label: `${host2.name}: install traefik` });
|
|
|
1865
1865
|
}
|
|
1866
1866
|
]
|
|
1867
1867
|
};
|
|
1868
|
-
await
|
|
1869
|
-
const jlist = await
|
|
1868
|
+
await host.writeFile(p.proxyEcosystem, JSON.stringify(ecosystem, null, 2), "600");
|
|
1869
|
+
const jlist = await host.capture(`${pathPrelude(cfg)} pm2 jlist 2>/dev/null || echo '[]'`, {
|
|
1870
1870
|
sudo: false,
|
|
1871
1871
|
allowFailure: true
|
|
1872
1872
|
});
|
|
1873
|
-
const running = await
|
|
1873
|
+
const running = await host.test(`${pathPrelude(cfg)} pm2 describe ${PROXY_PROCESS} >/dev/null 2>&1`);
|
|
1874
1874
|
if (!opts.force && running && jlist.includes(specHash)) {
|
|
1875
|
-
log.host(
|
|
1875
|
+
log.host(host.name, "proxy is up to date");
|
|
1876
1876
|
return;
|
|
1877
1877
|
}
|
|
1878
|
-
await
|
|
1878
|
+
await host.exec(`${pathPrelude(cfg)} pm2 delete ${PROXY_PROCESS} >/dev/null 2>&1 || true
|
|
1879
1879
|
${pathPrelude(cfg)} pm2 start ${shq(p.proxyEcosystem)} --update-env
|
|
1880
1880
|
${pathPrelude(cfg)} pm2 save --force >/dev/null 2>&1 || true
|
|
1881
1881
|
sleep 2
|
|
@@ -1883,29 +1883,29 @@ ${pathPrelude(cfg)} pm2 describe ${PROXY_PROCESS} | grep -q online || {
|
|
|
1883
1883
|
echo "traefik failed to start:" >&2
|
|
1884
1884
|
tail -n 40 ${shq(`${p.proxyLogDir}/err.log`)} >&2 2>/dev/null || true
|
|
1885
1885
|
exit 1
|
|
1886
|
-
}`, { sudo: false, label: `${
|
|
1887
|
-
log.host(
|
|
1886
|
+
}`, { sudo: false, label: `${host.name}: start proxy` });
|
|
1887
|
+
log.host(host.name, `proxy running (${entrypointSummary(merged)})`);
|
|
1888
1888
|
}
|
|
1889
1889
|
function entrypointSummary(d) {
|
|
1890
1890
|
return Object.entries(d.entrypoints).map(([name, port]) => `${name}:${port}`).join(" ");
|
|
1891
1891
|
}
|
|
1892
|
-
async function writeRoutes(
|
|
1892
|
+
async function writeRoutes(host, cfg, endpoints, accessories = []) {
|
|
1893
1893
|
if (!cfg.proxy.enabled)
|
|
1894
1894
|
return;
|
|
1895
1895
|
const doc = dynamicConfig(cfg, endpoints, accessories);
|
|
1896
|
-
await
|
|
1897
|
-
await
|
|
1896
|
+
await host.writeFile(paths(cfg).proxyDynamic, JSON.stringify(doc, null, 2), "644");
|
|
1897
|
+
await host.exec(`sleep ${Math.max(1, cfg.proxy.reloadWait)}`);
|
|
1898
1898
|
}
|
|
1899
|
-
async function removeRoutes(
|
|
1900
|
-
await
|
|
1899
|
+
async function removeRoutes(host, cfg) {
|
|
1900
|
+
await host.exec(`rm -f ${shq(paths(cfg).proxyDynamic)} ${shq(paths(cfg).proxyApp)}`, { allowFailure: true });
|
|
1901
1901
|
}
|
|
1902
|
-
async function proxyState(
|
|
1902
|
+
async function proxyState(host, cfg) {
|
|
1903
1903
|
if (cfg.mode === "docker") {
|
|
1904
|
-
const
|
|
1905
|
-
const [running, image,
|
|
1906
|
-
return { running: running === "true", image: image ?? "", status:
|
|
1904
|
+
const out = await host.capture(`docker inspect -f '{{.State.Running}}|{{.Config.Image}}|{{.State.Status}}' ${PROXY_CONTAINER} 2>/dev/null || echo 'false||absent'`, { allowFailure: true });
|
|
1905
|
+
const [running, image, status] = out.split("|");
|
|
1906
|
+
return { running: running === "true", image: image ?? "", status: status ?? "absent" };
|
|
1907
1907
|
}
|
|
1908
|
-
const out = await
|
|
1908
|
+
const out = await host.capture(`describe=$(${pathPrelude(cfg)} pm2 describe ${PROXY_PROCESS} 2>/dev/null || true)
|
|
1909
1909
|
printf '%s' "$describe" | awk -F'\u2502' '/status/ {gsub(/ /,"",$3); print $3; exit}' || true
|
|
1910
1910
|
version_out=$(${shq(paths(cfg).proxyBin)} version 2>/dev/null || true)
|
|
1911
1911
|
printf '%s' "$version_out" | awk '/[Vv]ersion:/ {print "traefik " $2; exit}' || true`, { sudo: false, allowFailure: true });
|
|
@@ -1931,8 +1931,8 @@ class DeployLock {
|
|
|
1931
1931
|
async acquire(what) {
|
|
1932
1932
|
const dir = paths(this.cfg).lock;
|
|
1933
1933
|
const info = { by: who(), at: new Date().toISOString(), what };
|
|
1934
|
-
for (const
|
|
1935
|
-
const res = await
|
|
1934
|
+
for (const host of this.hosts) {
|
|
1935
|
+
const res = await host.exec(`mkdir -p ${shq(paths(this.cfg).app)}
|
|
1936
1936
|
if mkdir ${shq(dir)} 2>/dev/null; then
|
|
1937
1937
|
printf '%s' ${shq(JSON.stringify(info))} > ${shq(dir)}/info
|
|
1938
1938
|
echo acquired
|
|
@@ -1946,27 +1946,27 @@ fi`, { allowFailure: true });
|
|
|
1946
1946
|
try {
|
|
1947
1947
|
held = JSON.parse(res.stdout || "{}");
|
|
1948
1948
|
} catch {}
|
|
1949
|
-
throw new EpdError(`${this.cfg.name} is locked on ${
|
|
1949
|
+
throw new EpdError(`${this.cfg.name} is locked on ${host.name}`, held.by ? `Held by ${held.by} since ${held.at} (${held.what}).
|
|
1950
1950
|
If that deploy died, run: epd lock release` : "Run `epd lock release` if a previous deploy was interrupted.");
|
|
1951
1951
|
}
|
|
1952
|
-
this.held.push(
|
|
1952
|
+
this.held.push(host);
|
|
1953
1953
|
}
|
|
1954
1954
|
log.debug(`lock acquired on ${this.held.length} host(s)`);
|
|
1955
1955
|
}
|
|
1956
1956
|
async release() {
|
|
1957
1957
|
const dir = paths(this.cfg).lock;
|
|
1958
|
-
for (const
|
|
1959
|
-
await
|
|
1958
|
+
for (const host of this.held.splice(0)) {
|
|
1959
|
+
await host.exec(`rm -rf ${shq(dir)}`, { allowFailure: true });
|
|
1960
1960
|
}
|
|
1961
1961
|
}
|
|
1962
1962
|
static async forceRelease(cfg, hosts) {
|
|
1963
|
-
for (const
|
|
1964
|
-
await
|
|
1963
|
+
for (const host of hosts)
|
|
1964
|
+
await host.exec(`rm -rf ${shq(paths(cfg).lock)}`, { allowFailure: true });
|
|
1965
1965
|
}
|
|
1966
1966
|
static async status(cfg, hosts) {
|
|
1967
1967
|
const out = [];
|
|
1968
|
-
for (const
|
|
1969
|
-
const raw = await
|
|
1968
|
+
for (const host of hosts) {
|
|
1969
|
+
const raw = await host.readFile(`${paths(cfg).lock}/info`);
|
|
1970
1970
|
let info = null;
|
|
1971
1971
|
if (raw) {
|
|
1972
1972
|
try {
|
|
@@ -1975,7 +1975,7 @@ If that deploy died, run: epd lock release` : "Run `epd lock release` if a previ
|
|
|
1975
1975
|
info = { by: "unknown", at: "unknown", what: "unknown" };
|
|
1976
1976
|
}
|
|
1977
1977
|
}
|
|
1978
|
-
out.push({ host:
|
|
1978
|
+
out.push({ host: host.name, info });
|
|
1979
1979
|
}
|
|
1980
1980
|
return out;
|
|
1981
1981
|
}
|
|
@@ -2074,34 +2074,34 @@ EPD_TARGETS
|
|
|
2074
2074
|
done
|
|
2075
2075
|
`;
|
|
2076
2076
|
}
|
|
2077
|
-
async function waitHealthy(
|
|
2077
|
+
async function waitHealthy(host, targets, health) {
|
|
2078
2078
|
if (!targets.length)
|
|
2079
2079
|
return;
|
|
2080
|
-
const res = await
|
|
2080
|
+
const res = await host.exec(healthScript(targets, health), {
|
|
2081
2081
|
allowFailure: true,
|
|
2082
|
-
label: `${
|
|
2082
|
+
label: `${host.name}: health check`
|
|
2083
2083
|
});
|
|
2084
2084
|
if (res.code === 0 && res.stdout.includes("EPD_HEALTHY"))
|
|
2085
2085
|
return;
|
|
2086
2086
|
const detail = res.stdout.replace("EPD_UNHEALTHY:", "").trim() || res.stderr;
|
|
2087
|
-
throw new EpdError(`${
|
|
2087
|
+
throw new EpdError(`${host.name}: new version did not become healthy within ${health.timeout}s`, `Still failing:${detail ? ` ${detail}` : ""}
|
|
2088
2088
|
Check \`epd logs\` \u2014 the old version is still serving traffic.`);
|
|
2089
2089
|
}
|
|
2090
2090
|
|
|
2091
2091
|
// src/core/state.ts
|
|
2092
|
-
async function readState(
|
|
2093
|
-
const raw = await
|
|
2092
|
+
async function readState(host, cfg) {
|
|
2093
|
+
const raw = await host.readFile(paths(cfg).state);
|
|
2094
2094
|
if (!raw)
|
|
2095
2095
|
return null;
|
|
2096
2096
|
try {
|
|
2097
2097
|
return JSON.parse(raw);
|
|
2098
2098
|
} catch {
|
|
2099
|
-
log.warn(`${
|
|
2099
|
+
log.warn(`${host.name}: state file is corrupt, treating this host as fresh`);
|
|
2100
2100
|
return null;
|
|
2101
2101
|
}
|
|
2102
2102
|
}
|
|
2103
|
-
async function writeState(
|
|
2104
|
-
await
|
|
2103
|
+
async function writeState(host, cfg, state) {
|
|
2104
|
+
await host.writeFile(paths(cfg).state, JSON.stringify(state, null, 2), "644");
|
|
2105
2105
|
}
|
|
2106
2106
|
function emptyState(cfg) {
|
|
2107
2107
|
return {
|
|
@@ -2210,7 +2210,7 @@ async function ensureBuilder() {
|
|
|
2210
2210
|
log.info('creating buildx builder "epd"');
|
|
2211
2211
|
await run(["docker", "buildx", "create", "--name", "epd", "--use", "--bootstrap"], { stream: true });
|
|
2212
2212
|
}
|
|
2213
|
-
async function loginRegistry(
|
|
2213
|
+
async function loginRegistry(host, cfg, env) {
|
|
2214
2214
|
if (!cfg.registry)
|
|
2215
2215
|
return;
|
|
2216
2216
|
const password = env[cfg.registry.passwordEnv];
|
|
@@ -2218,25 +2218,25 @@ async function loginRegistry(host2, cfg, env) {
|
|
|
2218
2218
|
throw new EpdError(`registry password: ${cfg.registry.passwordEnv} is not set`, "Export it, or put it in .env next to epd.yml.");
|
|
2219
2219
|
}
|
|
2220
2220
|
const server = cfg.registry.server ?? "";
|
|
2221
|
-
await
|
|
2221
|
+
await host.exec(`printf '%s' ${shq(password)} | docker login ${server ? shq(server) + " " : ""}--username ${shq(cfg.registry.username)} --password-stdin >/dev/null`, { label: `${host.name}: docker login` });
|
|
2222
2222
|
}
|
|
2223
|
-
async function ensureImage(
|
|
2223
|
+
async function ensureImage(host, cfg, version) {
|
|
2224
2224
|
const ref = imageRef(cfg, version);
|
|
2225
|
-
if (await
|
|
2226
|
-
log.host(
|
|
2225
|
+
if (await host.test(`docker image inspect ${shq(ref)} >/dev/null 2>&1`)) {
|
|
2226
|
+
log.host(host.name, `image ${version} already present`);
|
|
2227
2227
|
return;
|
|
2228
2228
|
}
|
|
2229
2229
|
if (cfg.registry) {
|
|
2230
|
-
log.host(
|
|
2231
|
-
await
|
|
2230
|
+
log.host(host.name, `pulling ${ref}`);
|
|
2231
|
+
await host.exec(`docker pull ${shq(ref)} >/dev/null`, { label: `${host.name}: docker pull` });
|
|
2232
2232
|
} else {
|
|
2233
|
-
log.host(
|
|
2234
|
-
await
|
|
2233
|
+
log.host(host.name, `shipping ${ref} over ssh (no registry configured)`);
|
|
2234
|
+
await host.pipeInto("gunzip | docker load", ["/bin/sh", "-c", `docker save ${shq(ref)} | gzip -1`]);
|
|
2235
2235
|
}
|
|
2236
2236
|
}
|
|
2237
|
-
async function pruneImages(
|
|
2237
|
+
async function pruneImages(host, cfg, keep, inUse) {
|
|
2238
2238
|
const keepRefs = inUse.map((v) => imageRef(cfg, v));
|
|
2239
|
-
await
|
|
2239
|
+
await host.exec(`keep=${shq(keepRefs.join(" "))}
|
|
2240
2240
|
docker images --filter "label=epd.app=${cfg.name}" --format '{{.Repository}}:{{.Tag}} {{.CreatedAt}}' 2>/dev/null \\
|
|
2241
2241
|
| sort -k2 -r | tail -n +$(( ${keep} + 1 )) | awk '{print $1}' | while read -r img; do
|
|
2242
2242
|
case " $keep " in *" $img "*) continue;; esac
|
|
@@ -2271,10 +2271,10 @@ function envFileContent(vars) {
|
|
|
2271
2271
|
return Object.entries(vars).map(([k, v]) => `${k}=${v}`).join(`
|
|
2272
2272
|
`);
|
|
2273
2273
|
}
|
|
2274
|
-
async function writeServiceEnv(
|
|
2274
|
+
async function writeServiceEnv(host, cfg, svc, env) {
|
|
2275
2275
|
const secrets = resolveSecrets(svc.secrets, env, `servers.${svc.name}`);
|
|
2276
2276
|
const file = `${paths(cfg).app}/env.${svc.name}`;
|
|
2277
|
-
await
|
|
2277
|
+
await host.writeFile(file, envFileContent({ ...svc.env, ...secrets }), "600");
|
|
2278
2278
|
return file;
|
|
2279
2279
|
}
|
|
2280
2280
|
function dockerRunArgs(o) {
|
|
@@ -2301,7 +2301,7 @@ function dockerRunArgs(o) {
|
|
|
2301
2301
|
args.push(...parseCommand(svc.command));
|
|
2302
2302
|
return args;
|
|
2303
2303
|
}
|
|
2304
|
-
async function startSlot(
|
|
2304
|
+
async function startSlot(host, cfg, svc, slot, version, envFile) {
|
|
2305
2305
|
const names = [];
|
|
2306
2306
|
const lines = [
|
|
2307
2307
|
`old=$(docker ps -a -q --filter "label=epd.app=${cfg.name}" --filter "label=epd.service=${svc.name}" --filter "label=epd.slot=${slot}")`,
|
|
@@ -2310,19 +2310,19 @@ async function startSlot(host2, cfg, svc, slot, version, envFile) {
|
|
|
2310
2310
|
for (let i = 0;i < svc.replicas; i++) {
|
|
2311
2311
|
const name = containerName(cfg.name, svc.name, slot, i);
|
|
2312
2312
|
names.push(name);
|
|
2313
|
-
const args = dockerRunArgs({ cfg, svc, slot, version, replica: i, serverHost:
|
|
2313
|
+
const args = dockerRunArgs({ cfg, svc, slot, version, replica: i, serverHost: host.name, envFile });
|
|
2314
2314
|
lines.push(`docker rm -f ${shq(name)} >/dev/null 2>&1 || true`);
|
|
2315
2315
|
lines.push(`${args.map((a) => shq(a)).join(" ")} >/dev/null`);
|
|
2316
2316
|
}
|
|
2317
|
-
await
|
|
2318
|
-
`), { label: `${
|
|
2317
|
+
await host.exec(lines.join(`
|
|
2318
|
+
`), { label: `${host.name}: start ${svc.name}/${slot}` });
|
|
2319
2319
|
return names;
|
|
2320
2320
|
}
|
|
2321
|
-
async function containerIps(
|
|
2321
|
+
async function containerIps(host, cfg, names) {
|
|
2322
2322
|
if (!names.length)
|
|
2323
2323
|
return {};
|
|
2324
2324
|
const tpl = `{{ with (index .NetworkSettings.Networks ${JSON.stringify(cfg.proxy.network)}) }}{{ .IPAddress }}{{ end }}`;
|
|
2325
|
-
const out = await
|
|
2325
|
+
const out = await host.capture(names.map((n) => `echo "${n} $(docker inspect -f ${shq(tpl)} ${shq(n)} 2>/dev/null)"`).join(`
|
|
2326
2326
|
`));
|
|
2327
2327
|
const map = {};
|
|
2328
2328
|
for (const line of out.split(`
|
|
@@ -2333,21 +2333,21 @@ async function containerIps(host2, cfg, names) {
|
|
|
2333
2333
|
}
|
|
2334
2334
|
return map;
|
|
2335
2335
|
}
|
|
2336
|
-
async function stopSlot(
|
|
2336
|
+
async function stopSlot(host, cfg, svc, slot, opts) {
|
|
2337
2337
|
const teardown = `names=$(docker ps -a -q --filter "label=epd.app=${cfg.name}" --filter "label=epd.service=${svc.name}" --filter "label=epd.slot=${slot}")
|
|
2338
2338
|
if [ -n "$names" ]; then
|
|
2339
2339
|
sleep ${opts.drain}
|
|
2340
2340
|
docker stop --timeout ${svc.stopTimeout} $names >/dev/null 2>&1 || true
|
|
2341
2341
|
${opts.remove ? "docker rm -f $names >/dev/null 2>&1 || true" : ""}
|
|
2342
2342
|
fi`;
|
|
2343
|
-
await
|
|
2344
|
-
disown 2>/dev/null || true` : teardown, { allowFailure: true, label: `${
|
|
2343
|
+
await host.exec(opts.detach ? `nohup setsid bash -c ${shq(teardown)} >/dev/null 2>&1 < /dev/null &
|
|
2344
|
+
disown 2>/dev/null || true` : teardown, { allowFailure: true, label: `${host.name}: stop ${svc.name}/${slot}` });
|
|
2345
2345
|
}
|
|
2346
|
-
async function removeApp(
|
|
2347
|
-
await
|
|
2346
|
+
async function removeApp(host, cfg) {
|
|
2347
|
+
await host.exec(`ids=$(docker ps -a -q --filter "label=epd.app=${cfg.name}")
|
|
2348
2348
|
[ -n "$ids" ] && docker rm -f $ids >/dev/null 2>&1 || true`, { allowFailure: true });
|
|
2349
2349
|
}
|
|
2350
|
-
async function listContainers(
|
|
2350
|
+
async function listContainers(host, cfg) {
|
|
2351
2351
|
const fmt = [
|
|
2352
2352
|
"{{.Names}}",
|
|
2353
2353
|
'{{.Label "epd.service"}}',
|
|
@@ -2359,7 +2359,7 @@ async function listContainers(host2, cfg) {
|
|
|
2359
2359
|
"{{.Image}}",
|
|
2360
2360
|
"{{.CreatedAt}}"
|
|
2361
2361
|
].join("\t");
|
|
2362
|
-
const out = await
|
|
2362
|
+
const out = await host.capture(`docker ps -a --filter "label=epd.app=${cfg.name}" --format ${shq(fmt)} 2>/dev/null || true`, { allowFailure: true });
|
|
2363
2363
|
return out.split(`
|
|
2364
2364
|
`).filter(Boolean).map((line) => {
|
|
2365
2365
|
const [name, service, slot, version, replica, status, state, image, created] = line.split("\t");
|
|
@@ -2376,11 +2376,11 @@ async function listContainers(host2, cfg) {
|
|
|
2376
2376
|
};
|
|
2377
2377
|
});
|
|
2378
2378
|
}
|
|
2379
|
-
async function startAccessory(
|
|
2379
|
+
async function startAccessory(host, cfg, acc, env, opts) {
|
|
2380
2380
|
const name = accessoryContainer(cfg.name, acc.name);
|
|
2381
2381
|
const secrets = resolveSecrets(acc.secrets, env, `accessories.${acc.name}`);
|
|
2382
2382
|
const envFile = `${paths(cfg).app}/env.acc.${acc.name}`;
|
|
2383
|
-
await
|
|
2383
|
+
await host.writeFile(envFile, envFileContent({ ...acc.env, ...secrets }), "600");
|
|
2384
2384
|
const args = [
|
|
2385
2385
|
"docker",
|
|
2386
2386
|
"run",
|
|
@@ -2411,12 +2411,12 @@ async function startAccessory(host2, cfg, acc, env, opts) {
|
|
|
2411
2411
|
args.push(...acc.dockerOptions, acc.image);
|
|
2412
2412
|
if (acc.command)
|
|
2413
2413
|
args.push(...parseCommand(acc.command));
|
|
2414
|
-
await
|
|
2414
|
+
await host.exec(`if docker inspect ${shq(name)} >/dev/null 2>&1; then
|
|
2415
2415
|
${opts.recreate ? `docker rm -f ${shq(name)} >/dev/null` : `docker start ${shq(name)} >/dev/null 2>&1 || true; exit 0`}
|
|
2416
2416
|
fi
|
|
2417
2417
|
docker pull ${shq(acc.image)} >/dev/null
|
|
2418
|
-
${args.map((a) => shq(a)).join(" ")} >/dev/null`, { label: `${
|
|
2419
|
-
log.host(
|
|
2418
|
+
${args.map((a) => shq(a)).join(" ")} >/dev/null`, { label: `${host.name}: accessory ${acc.name}` });
|
|
2419
|
+
log.host(host.name, `accessory ${acc.name} running`);
|
|
2420
2420
|
}
|
|
2421
2421
|
|
|
2422
2422
|
// src/deploy/process.ts
|
|
@@ -2427,34 +2427,34 @@ var pm2 = (cfg, args) => `${pathPrelude(cfg)} pm2 ${args}`;
|
|
|
2427
2427
|
function releaseDir(cfg, version) {
|
|
2428
2428
|
return `${paths(cfg).releases}/${version}`;
|
|
2429
2429
|
}
|
|
2430
|
-
async function uploadSource(
|
|
2430
|
+
async function uploadSource(host, cfg, version) {
|
|
2431
2431
|
const dest = releaseDir(cfg, version);
|
|
2432
2432
|
const p = paths(cfg);
|
|
2433
|
-
await
|
|
2433
|
+
await host.exec(`mkdir -p ${shq(p.releases)} ${shq(p.shared)}`, asUser);
|
|
2434
2434
|
if (cfg.process.source === "git") {
|
|
2435
2435
|
if (!existsSync4(join5(cfg.root, ".git")))
|
|
2436
2436
|
throw new EpdError('process.source is "git" but this is not a git repository');
|
|
2437
|
-
log.host(
|
|
2438
|
-
await
|
|
2437
|
+
log.host(host.name, `uploading HEAD as ${version}`);
|
|
2438
|
+
await host.pipeInto(`mkdir -p ${shq(dest)} && tar -x -C ${shq(dest)}`, ["/bin/sh", "-c", `git -C ${shq(cfg.root)} archive --format=tar HEAD`]);
|
|
2439
2439
|
return;
|
|
2440
2440
|
}
|
|
2441
|
-
const previous = await
|
|
2441
|
+
const previous = await host.capture(`ls -1dt ${shq(p.releases)}/*/ 2>/dev/null | head -n 1 || true`, { ...asUser, allowFailure: true });
|
|
2442
2442
|
const args = ["rsync", "-az", "--delete", "--human-readable", "--info=stats1"];
|
|
2443
2443
|
for (const ex of cfg.process.exclude)
|
|
2444
2444
|
args.push("--exclude", ex);
|
|
2445
2445
|
if (previous)
|
|
2446
2446
|
args.push("--link-dest", previous.replace(/\/$/, ""));
|
|
2447
|
-
args.push("-e",
|
|
2448
|
-
args.push(`${cfg.root.replace(/\/$/, "")}/`, `${
|
|
2449
|
-
await
|
|
2450
|
-
log.host(
|
|
2451
|
-
await run(args, { cwd: cfg.root, label: `rsync \u2192 ${
|
|
2447
|
+
args.push("-e", host.rsyncShell());
|
|
2448
|
+
args.push(`${cfg.root.replace(/\/$/, "")}/`, `${host.target}:${dest}/`);
|
|
2449
|
+
await host.exec(`mkdir -p ${shq(dest)}`, asUser);
|
|
2450
|
+
log.host(host.name, `uploading ${version}`);
|
|
2451
|
+
await run(args, { cwd: cfg.root, label: `rsync \u2192 ${host.name}` });
|
|
2452
2452
|
}
|
|
2453
|
-
async function buildRelease(
|
|
2453
|
+
async function buildRelease(host, cfg, version, env) {
|
|
2454
2454
|
const dest = releaseDir(cfg, version);
|
|
2455
2455
|
const secrets = resolveSecrets(cfg.secrets, env, "env.secret");
|
|
2456
2456
|
const envFile = paths(cfg).envFile;
|
|
2457
|
-
await
|
|
2457
|
+
await host.writeFile(envFile, Object.entries({ ...cfg.env, ...secrets }).map(([k, v]) => `${k}=${v}`).join(`
|
|
2458
2458
|
`), "600");
|
|
2459
2459
|
const steps = [`cd ${shq(dest)}`, pathPrelude(cfg), `set -a; . ${shq(envFile)}; set +a`];
|
|
2460
2460
|
if (cfg.process.install)
|
|
@@ -2462,9 +2462,9 @@ async function buildRelease(host2, cfg, version, env) {
|
|
|
2462
2462
|
if (cfg.process.build)
|
|
2463
2463
|
steps.push(cfg.process.build);
|
|
2464
2464
|
if (steps.length > 3) {
|
|
2465
|
-
log.host(
|
|
2466
|
-
await
|
|
2467
|
-
`), { ...asUser, stream: true, label: `${
|
|
2465
|
+
log.host(host.name, "installing and building");
|
|
2466
|
+
await host.exec(steps.join(`
|
|
2467
|
+
`), { ...asUser, stream: true, label: `${host.name}: build` });
|
|
2468
2468
|
}
|
|
2469
2469
|
}
|
|
2470
2470
|
function ecosystem(cfg, svc, slot, version, env) {
|
|
@@ -2502,34 +2502,34 @@ function ecosystem(cfg, svc, slot, version, env) {
|
|
|
2502
2502
|
}
|
|
2503
2503
|
return { apps };
|
|
2504
2504
|
}
|
|
2505
|
-
async function startSlot2(
|
|
2505
|
+
async function startSlot2(host, cfg, svc, slot, version, env) {
|
|
2506
2506
|
const secrets = resolveSecrets(svc.secrets, env, `servers.${svc.name}`);
|
|
2507
2507
|
const doc = ecosystem(cfg, svc, slot, version, { ...svc.env, ...secrets });
|
|
2508
2508
|
const file = `${paths(cfg).app}/pm2.${svc.name}.${slot}.json`;
|
|
2509
|
-
await
|
|
2509
|
+
await host.writeFile(file, JSON.stringify(doc, null, 2), "600");
|
|
2510
2510
|
const stale = Array.from({ length: 16 }, (_, i) => processName(cfg.name, svc.name, slot, i));
|
|
2511
|
-
await
|
|
2511
|
+
await host.exec(`for name in ${stale.map((n) => shq(n)).join(" ")}; do ${pathPrelude(cfg)} pm2 delete "$name" >/dev/null 2>&1 || true; done
|
|
2512
2512
|
${pm2(cfg, `start ${shq(file)} --update-env`)}
|
|
2513
|
-
${pm2(cfg, "save --force")} >/dev/null 2>&1 || true`, { ...asUser, label: `${
|
|
2513
|
+
${pm2(cfg, "save --force")} >/dev/null 2>&1 || true`, { ...asUser, label: `${host.name}: pm2 start ${svc.name}/${slot}` });
|
|
2514
2514
|
}
|
|
2515
|
-
async function stopSlot2(
|
|
2515
|
+
async function stopSlot2(host, cfg, svc, slot, drain, opts = {}) {
|
|
2516
2516
|
const names = Array.from({ length: 16 }, (_, i) => processName(cfg.name, svc.name, slot, i));
|
|
2517
2517
|
const teardown = `sleep ${drain}
|
|
2518
2518
|
for name in ${names.map((n) => shq(n)).join(" ")}; do
|
|
2519
2519
|
${pathPrelude(cfg)} pm2 delete "$name" >/dev/null 2>&1 || true
|
|
2520
2520
|
done
|
|
2521
2521
|
${pm2(cfg, "save --force")} >/dev/null 2>&1 || true`;
|
|
2522
|
-
await
|
|
2523
|
-
disown 2>/dev/null || true` : teardown, { ...asUser, allowFailure: true, label: `${
|
|
2522
|
+
await host.exec(opts.detach ? `nohup setsid bash -c ${shq(teardown)} >/dev/null 2>&1 < /dev/null &
|
|
2523
|
+
disown 2>/dev/null || true` : teardown, { ...asUser, allowFailure: true, label: `${host.name}: pm2 stop ${svc.name}/${slot}` });
|
|
2524
2524
|
}
|
|
2525
|
-
async function removeApp2(
|
|
2526
|
-
await
|
|
2525
|
+
async function removeApp2(host, cfg) {
|
|
2526
|
+
await host.exec(`${pathPrelude(cfg)} pm2 jlist 2>/dev/null | tr ',' '\\n' | grep -o '"name":"epd-${cfg.name}-[^"]*"' | cut -d'"' -f4 | sort -u | while read -r n; do
|
|
2527
2527
|
${pathPrelude(cfg)} pm2 delete "$n" >/dev/null 2>&1 || true
|
|
2528
2528
|
done
|
|
2529
2529
|
${pm2(cfg, "save --force")} >/dev/null 2>&1 || true`, { ...asUser, allowFailure: true });
|
|
2530
2530
|
}
|
|
2531
|
-
async function listProcesses(
|
|
2532
|
-
const out = await
|
|
2531
|
+
async function listProcesses(host, cfg) {
|
|
2532
|
+
const out = await host.capture(`${pm2(cfg, "jlist")} 2>/dev/null || echo '[]'`, { ...asUser, allowFailure: true });
|
|
2533
2533
|
const jsonStart = out.indexOf("[");
|
|
2534
2534
|
let parsed = [];
|
|
2535
2535
|
try {
|
|
@@ -2546,9 +2546,9 @@ async function listProcesses(host2, cfg) {
|
|
|
2546
2546
|
memory: p.monit?.memory ?? 0
|
|
2547
2547
|
}));
|
|
2548
2548
|
}
|
|
2549
|
-
async function finalizeRelease(
|
|
2549
|
+
async function finalizeRelease(host, cfg, version) {
|
|
2550
2550
|
const p = paths(cfg);
|
|
2551
|
-
await
|
|
2551
|
+
await host.exec(`ln -sfn ${shq(releaseDir(cfg, version))} ${shq(p.current)}.tmp && mv -Tf ${shq(p.current)}.tmp ${shq(p.current)}
|
|
2552
2552
|
ls -1dt ${shq(p.releases)}/*/ 2>/dev/null | tail -n +$(( ${cfg.keepReleases} + 1 )) | while read -r d; do
|
|
2553
2553
|
case "$d" in *${shq(version)}*) continue;; esac
|
|
2554
2554
|
rm -rf "$d"
|
|
@@ -2558,7 +2558,7 @@ done`, { ...asUser, allowFailure: true });
|
|
|
2558
2558
|
// src/deploy/deploy.ts
|
|
2559
2559
|
async function deploy(loaded, opts = {}) {
|
|
2560
2560
|
const { config: cfg, env } = loaded;
|
|
2561
|
-
const
|
|
2561
|
+
const started = Date.now();
|
|
2562
2562
|
const services = selectServices(cfg, opts.onlyServices);
|
|
2563
2563
|
const targetHosts = selectHosts(cfg, services, opts.onlyHosts);
|
|
2564
2564
|
const hosts = targetHosts.map((h) => host(h, cfg.ssh));
|
|
@@ -2575,15 +2575,15 @@ async function deploy(loaded, opts = {}) {
|
|
|
2575
2575
|
await hook(cfg, "pre_deploy", cfg.hooks.preDeploy, version);
|
|
2576
2576
|
await withLock(cfg, hosts, `deploy ${version}`, opts.skipLock ?? false, async () => {
|
|
2577
2577
|
log.step("Preparing servers");
|
|
2578
|
-
await pool(hosts, 8, async (
|
|
2579
|
-
await verifyHost(
|
|
2578
|
+
await pool(hosts, 8, async (host) => {
|
|
2579
|
+
await verifyHost(host, cfg);
|
|
2580
2580
|
if (cfg.mode === "docker")
|
|
2581
|
-
await ensureNetwork(
|
|
2581
|
+
await ensureNetwork(host, cfg);
|
|
2582
2582
|
});
|
|
2583
2583
|
await deployAccessories(cfg, env, opts.recreateAccessories ?? false);
|
|
2584
2584
|
if (cfg.proxy.enabled && proxyHosts(cfg).length) {
|
|
2585
2585
|
log.step("Checking proxy");
|
|
2586
|
-
await pool(proxyHosts(cfg).filter((h) => targetHosts.includes(h)).map((h) => host(h, cfg.ssh)), 4, (
|
|
2586
|
+
await pool(proxyHosts(cfg).filter((h) => targetHosts.includes(h)).map((h) => host(h, cfg.ssh)), 4, (host) => ensureProxy(host, cfg, { secrets: env }));
|
|
2587
2587
|
}
|
|
2588
2588
|
const live = new Map;
|
|
2589
2589
|
for (const h of allHosts(cfg)) {
|
|
@@ -2593,61 +2593,61 @@ async function deploy(loaded, opts = {}) {
|
|
|
2593
2593
|
m.set(svc, s.slot);
|
|
2594
2594
|
live.set(h, m);
|
|
2595
2595
|
}
|
|
2596
|
-
const deployHost = async (
|
|
2597
|
-
await deployToHost({ cfg, env, host
|
|
2596
|
+
const deployHost = async (host) => {
|
|
2597
|
+
await deployToHost({ cfg, env, host, services, version, opts, live });
|
|
2598
2598
|
};
|
|
2599
2599
|
if (cfg.strategy === "parallel") {
|
|
2600
2600
|
await pool(hosts, hosts.length, deployHost);
|
|
2601
2601
|
} else {
|
|
2602
|
-
for (const
|
|
2603
|
-
await deployHost(
|
|
2602
|
+
for (const host of hosts)
|
|
2603
|
+
await deployHost(host);
|
|
2604
2604
|
}
|
|
2605
2605
|
if (cfg.proxy.crossHost) {
|
|
2606
2606
|
log.step("Syncing routes across hosts");
|
|
2607
|
-
await pool(proxyHosts(cfg).map((h) => host(h, cfg.ssh)), 4, async (
|
|
2608
|
-
await writeRoutes(
|
|
2607
|
+
await pool(proxyHosts(cfg).map((h) => host(h, cfg.ssh)), 4, async (host) => {
|
|
2608
|
+
await writeRoutes(host, cfg, buildEndpoints(cfg, host.name, live), buildAccessoryEndpoints(cfg, host.name));
|
|
2609
2609
|
});
|
|
2610
2610
|
}
|
|
2611
2611
|
});
|
|
2612
2612
|
if (!opts.skipHooks && cfg.hooks.postDeploy)
|
|
2613
2613
|
await hook(cfg, "post_deploy", cfg.hooks.postDeploy, version);
|
|
2614
|
-
log.ok(`Deployed ${cfg.name} ${version} in ${dur(Date.now() -
|
|
2614
|
+
log.ok(`Deployed ${cfg.name} ${version} in ${dur(Date.now() - started)}`);
|
|
2615
2615
|
printUrls(cfg);
|
|
2616
2616
|
}
|
|
2617
2617
|
async function deployToHost(d) {
|
|
2618
|
-
const { cfg, env, host
|
|
2619
|
-
const services = d.services.filter((s) => s.hosts.includes(
|
|
2618
|
+
const { cfg, env, host, version, opts, live } = d;
|
|
2619
|
+
const services = d.services.filter((s) => s.hosts.includes(host.name));
|
|
2620
2620
|
if (!services.length)
|
|
2621
2621
|
return;
|
|
2622
|
-
log.step(`${
|
|
2622
|
+
log.step(`${host.name}: deploying ${services.map((s) => s.name).join(", ")}`);
|
|
2623
2623
|
if (cfg.mode === "docker") {
|
|
2624
|
-
await loginRegistry(
|
|
2625
|
-
await ensureImage(
|
|
2624
|
+
await loginRegistry(host, cfg, env);
|
|
2625
|
+
await ensureImage(host, cfg, version);
|
|
2626
2626
|
} else if (!opts.reuse) {
|
|
2627
|
-
await uploadSource(
|
|
2628
|
-
await buildRelease(
|
|
2627
|
+
await uploadSource(host, cfg, version);
|
|
2628
|
+
await buildRelease(host, cfg, version, env);
|
|
2629
2629
|
} else {
|
|
2630
2630
|
const dir = releaseDir(cfg, version);
|
|
2631
|
-
if (!await
|
|
2632
|
-
throw new EpdError(`${
|
|
2631
|
+
if (!await host.test(`[ -d ${JSON.stringify(dir)} ]`)) {
|
|
2632
|
+
throw new EpdError(`${host.name}: release ${version} is not on this server`, "Deploy it normally first.");
|
|
2633
2633
|
}
|
|
2634
2634
|
}
|
|
2635
|
-
const state = await readState(
|
|
2635
|
+
const state = await readState(host, cfg) ?? emptyState(cfg);
|
|
2636
2636
|
const previous = new Map;
|
|
2637
|
-
const
|
|
2637
|
+
const started = [];
|
|
2638
2638
|
try {
|
|
2639
2639
|
for (const svc of services) {
|
|
2640
2640
|
const current = state.services[svc.name]?.slot;
|
|
2641
2641
|
if (current)
|
|
2642
2642
|
previous.set(svc.name, current);
|
|
2643
2643
|
const slot = current ? otherSlot(current) : "blue";
|
|
2644
|
-
log.host(
|
|
2644
|
+
log.host(host.name, `${svc.name}: starting ${svc.replicas} replica(s) on ${slot}`);
|
|
2645
2645
|
let probes = [];
|
|
2646
2646
|
if (cfg.mode === "docker") {
|
|
2647
|
-
const envFile = await writeServiceEnv(
|
|
2648
|
-
const names = await startSlot(
|
|
2649
|
-
|
|
2650
|
-
const ips = usesHostPorts(cfg) ? {} : await containerIps(
|
|
2647
|
+
const envFile = await writeServiceEnv(host, cfg, svc, env);
|
|
2648
|
+
const names = await startSlot(host, cfg, svc, slot, version, envFile);
|
|
2649
|
+
started.push({ svc, slot });
|
|
2650
|
+
const ips = usesHostPorts(cfg) ? {} : await containerIps(host, cfg, names);
|
|
2651
2651
|
probes = names.map((name, i) => ({
|
|
2652
2652
|
label: name,
|
|
2653
2653
|
address: usesHostPorts(cfg) ? "127.0.0.1" : ips[name] ?? "",
|
|
@@ -2655,10 +2655,10 @@ async function deployToHost(d) {
|
|
|
2655
2655
|
}));
|
|
2656
2656
|
const noIp = probes.find((p) => !p.address);
|
|
2657
2657
|
if (noIp)
|
|
2658
|
-
throw new EpdError(`${
|
|
2658
|
+
throw new EpdError(`${host.name}: ${noIp.label} did not get an IP on the ${cfg.proxy.network} network`, "Check `epd logs`.");
|
|
2659
2659
|
} else {
|
|
2660
|
-
await startSlot2(
|
|
2661
|
-
|
|
2660
|
+
await startSlot2(host, cfg, svc, slot, version, env);
|
|
2661
|
+
started.push({ svc, slot });
|
|
2662
2662
|
probes = Array.from({ length: svc.replicas }, (_, i) => ({
|
|
2663
2663
|
label: `${svc.name}#${i}`,
|
|
2664
2664
|
address: "127.0.0.1",
|
|
@@ -2666,52 +2666,52 @@ async function deployToHost(d) {
|
|
|
2666
2666
|
}));
|
|
2667
2667
|
}
|
|
2668
2668
|
if (!opts.skipHealth && svc.proxied) {
|
|
2669
|
-
log.host(
|
|
2670
|
-
await waitHealthy(
|
|
2671
|
-
log.host(
|
|
2669
|
+
log.host(host.name, `${svc.name}: waiting for health check${svc.health.path ? ` ${svc.health.path}` : " (tcp)"}`);
|
|
2670
|
+
await waitHealthy(host, probes, svc.health);
|
|
2671
|
+
log.host(host.name, `${svc.name}: healthy`);
|
|
2672
2672
|
} else if (!opts.skipHealth) {
|
|
2673
|
-
await confirmUp(
|
|
2673
|
+
await confirmUp(host, cfg, svc, slot);
|
|
2674
2674
|
}
|
|
2675
|
-
live.get(
|
|
2675
|
+
live.get(host.name)?.set(svc.name, slot) ?? live.set(host.name, new Map([[svc.name, slot]]));
|
|
2676
2676
|
state.services[svc.name] = {
|
|
2677
2677
|
slot,
|
|
2678
2678
|
replicas: svc.replicas,
|
|
2679
2679
|
version,
|
|
2680
|
-
endpoints: endpointsFor(cfg, svc, slot,
|
|
2680
|
+
endpoints: endpointsFor(cfg, svc, slot, host.name).map((e) => e.url)
|
|
2681
2681
|
};
|
|
2682
2682
|
}
|
|
2683
2683
|
const hasSvcRoutes = services.some((s) => s.routes.length);
|
|
2684
|
-
const hasAccRoutes = cfg.accessories.some((a) => a.routes.length && (cfg.proxy.crossHost || a.host ===
|
|
2684
|
+
const hasAccRoutes = cfg.accessories.some((a) => a.routes.length && (cfg.proxy.crossHost || a.host === host.name));
|
|
2685
2685
|
if (cfg.proxy.enabled && (hasSvcRoutes || hasAccRoutes)) {
|
|
2686
|
-
await writeRoutes(
|
|
2687
|
-
log.host(
|
|
2686
|
+
await writeRoutes(host, cfg, buildEndpoints(cfg, host.name, live), buildAccessoryEndpoints(cfg, host.name));
|
|
2687
|
+
log.host(host.name, "routes updated");
|
|
2688
2688
|
}
|
|
2689
2689
|
for (const svc of services) {
|
|
2690
2690
|
const old = previous.get(svc.name);
|
|
2691
2691
|
if (!old)
|
|
2692
2692
|
continue;
|
|
2693
2693
|
const drain = Math.max(svc.drain, cfg.proxy.reloadWait + 2);
|
|
2694
|
-
log.host(
|
|
2694
|
+
log.host(host.name, `${svc.name}: retiring ${old} (${drain}s drain, in the background)`);
|
|
2695
2695
|
if (cfg.mode === "docker")
|
|
2696
|
-
await stopSlot(
|
|
2696
|
+
await stopSlot(host, cfg, svc, old, { drain, remove: true, detach: true });
|
|
2697
2697
|
else
|
|
2698
|
-
await stopSlot2(
|
|
2698
|
+
await stopSlot2(host, cfg, svc, old, drain, { detach: true });
|
|
2699
2699
|
}
|
|
2700
|
-
await writeState(
|
|
2700
|
+
await writeState(host, cfg, recordDeploy(state, version, process.env.USER ?? "epd", cfg.keepReleases));
|
|
2701
2701
|
if (cfg.mode === "docker") {
|
|
2702
|
-
await pruneImages(
|
|
2702
|
+
await pruneImages(host, cfg, cfg.keepReleases, [version, ...state.history.slice(0, 2).map((h) => h.version)]);
|
|
2703
2703
|
} else {
|
|
2704
|
-
await finalizeRelease(
|
|
2704
|
+
await finalizeRelease(host, cfg, version);
|
|
2705
2705
|
}
|
|
2706
|
-
log.host(
|
|
2706
|
+
log.host(host.name, c.green("done"));
|
|
2707
2707
|
} catch (error) {
|
|
2708
|
-
log.error(`${
|
|
2709
|
-
for (const { svc, slot } of
|
|
2708
|
+
log.error(`${host.name}: deploy failed, rolling back this host`);
|
|
2709
|
+
for (const { svc, slot } of started) {
|
|
2710
2710
|
try {
|
|
2711
2711
|
if (cfg.mode === "docker")
|
|
2712
|
-
await stopSlot(
|
|
2712
|
+
await stopSlot(host, cfg, svc, slot, { drain: 0, remove: true });
|
|
2713
2713
|
else
|
|
2714
|
-
await stopSlot2(
|
|
2714
|
+
await stopSlot2(host, cfg, svc, slot, 0);
|
|
2715
2715
|
} catch {}
|
|
2716
2716
|
}
|
|
2717
2717
|
if (!d.opts.skipHooks && cfg.hooks.onFailure) {
|
|
@@ -2722,18 +2722,18 @@ async function deployToHost(d) {
|
|
|
2722
2722
|
throw error;
|
|
2723
2723
|
}
|
|
2724
2724
|
}
|
|
2725
|
-
async function confirmUp(
|
|
2725
|
+
async function confirmUp(host, cfg, svc, slot) {
|
|
2726
2726
|
if (cfg.mode === "docker") {
|
|
2727
|
-
const containers = await listContainers(
|
|
2727
|
+
const containers = await listContainers(host, cfg);
|
|
2728
2728
|
const bad = containers.filter((ct) => ct.service === svc.name && ct.slot === slot && !ct.running);
|
|
2729
2729
|
if (bad.length) {
|
|
2730
|
-
throw new EpdError(`${
|
|
2730
|
+
throw new EpdError(`${host.name}: ${bad.map((b) => b.name).join(", ")} exited right after starting`, `Run: epd logs --service ${svc.name}`);
|
|
2731
2731
|
}
|
|
2732
2732
|
} else {
|
|
2733
|
-
const procs = await listProcesses(
|
|
2733
|
+
const procs = await listProcesses(host, cfg);
|
|
2734
2734
|
const bad = procs.filter((p) => p.name.includes(`-${svc.name}-${slot}-`) && p.status !== "online");
|
|
2735
2735
|
if (bad.length) {
|
|
2736
|
-
throw new EpdError(`${
|
|
2736
|
+
throw new EpdError(`${host.name}: ${bad.map((b) => b.name).join(", ")} is ${bad[0].status}`, `Run: epd logs --service ${svc.name}`);
|
|
2737
2737
|
}
|
|
2738
2738
|
}
|
|
2739
2739
|
}
|
|
@@ -2874,22 +2874,22 @@ Options
|
|
|
2874
2874
|
log.step(`Preparing ${hosts.length} host(s) for ${c.bold(cfg.name)}`);
|
|
2875
2875
|
if (cfg.mode === "docker")
|
|
2876
2876
|
await requireLocal("docker", "Install Docker Desktop or the docker engine to build images.");
|
|
2877
|
-
await pool(hosts, 4, async (
|
|
2878
|
-
await
|
|
2879
|
-
log.host(
|
|
2877
|
+
await pool(hosts, 4, async (host) => {
|
|
2878
|
+
await host.ping();
|
|
2879
|
+
log.host(host.name, "reachable");
|
|
2880
2880
|
if (cfg.mode === "docker") {
|
|
2881
|
-
await installDocker(
|
|
2882
|
-
await ensureNetwork(
|
|
2881
|
+
await installDocker(host);
|
|
2882
|
+
await ensureNetwork(host, cfg);
|
|
2883
2883
|
} else {
|
|
2884
|
-
await installSystemPackages(
|
|
2885
|
-
await installProcessRuntime(
|
|
2884
|
+
await installSystemPackages(host);
|
|
2885
|
+
await installProcessRuntime(host, cfg);
|
|
2886
2886
|
}
|
|
2887
|
-
await ensureDirs(
|
|
2888
|
-
await reservePorts(
|
|
2887
|
+
await ensureDirs(host, cfg);
|
|
2888
|
+
await reservePorts(host, cfg);
|
|
2889
2889
|
});
|
|
2890
2890
|
if (cfg.proxy.enabled && proxyHosts(cfg).length) {
|
|
2891
2891
|
log.step("Starting the proxy");
|
|
2892
|
-
await pool(proxyHosts(cfg).filter((h) => names.includes(h)).map((h) => host(h, cfg.ssh)), 4, (
|
|
2892
|
+
await pool(proxyHosts(cfg).filter((h) => names.includes(h)).map((h) => host(h, cfg.ssh)), 4, (host) => ensureProxy(host, cfg, { secrets: loaded.env, force: true }));
|
|
2893
2893
|
}
|
|
2894
2894
|
log.ok("servers are ready");
|
|
2895
2895
|
if (ctx.values["skip-deploy"]) {
|
|
@@ -3250,12 +3250,12 @@ Examples
|
|
|
3250
3250
|
const known = cfg.accessories.map((a) => a.name);
|
|
3251
3251
|
throw new EpdError(`no such accessory "${accName}"`, known.length ? `Known: ${known.join(", ")}` : "No accessories configured.");
|
|
3252
3252
|
}
|
|
3253
|
-
const
|
|
3253
|
+
const host2 = host(acc.host, cfg.ssh);
|
|
3254
3254
|
const container = accessoryContainer(cfg.name, acc.name);
|
|
3255
|
-
log.info(`${
|
|
3256
|
-
const
|
|
3257
|
-
if (
|
|
3258
|
-
process.exitCode =
|
|
3255
|
+
log.info(`${host2.name}: ${container} $ ${command}`);
|
|
3256
|
+
const code = await host2.interactive(`docker exec ${ctx.values.interactive ? "-it" : ""} ${shq(container)} ${cmdArgs}`);
|
|
3257
|
+
if (code !== 0)
|
|
3258
|
+
process.exitCode = code;
|
|
3259
3259
|
return;
|
|
3260
3260
|
}
|
|
3261
3261
|
const host2 = await pickHost(ctx, cfg);
|
|
@@ -3270,18 +3270,18 @@ Examples
|
|
|
3270
3270
|
const dir = `${paths(cfg).current}`;
|
|
3271
3271
|
const script = `cd ${shq(dir)} && ${pathPrelude(cfg)} && set -a && . ${shq(paths(cfg).envFile)} && set +a && ${command}`;
|
|
3272
3272
|
log.info(`${host2.name}: ${command}`);
|
|
3273
|
-
const
|
|
3274
|
-
if (
|
|
3275
|
-
process.exitCode =
|
|
3273
|
+
const code = await host2.interactive(script, { sudo: false });
|
|
3274
|
+
if (code !== 0)
|
|
3275
|
+
process.exitCode = code;
|
|
3276
3276
|
return;
|
|
3277
3277
|
}
|
|
3278
3278
|
if (ctx.values.reuse) {
|
|
3279
3279
|
const slot = state.services[svc.name]?.slot;
|
|
3280
3280
|
const container = `epd-${cfg.name}-${svc.name}-${slot}-0`;
|
|
3281
3281
|
log.info(`${host2.name}: ${container} $ ${command}`);
|
|
3282
|
-
const
|
|
3283
|
-
if (
|
|
3284
|
-
process.exitCode =
|
|
3282
|
+
const code = await host2.interactive(`docker exec ${ctx.values.interactive ? "-it" : ""} ${shq(container)} ${cmdArgs}`);
|
|
3283
|
+
if (code !== 0)
|
|
3284
|
+
process.exitCode = code;
|
|
3285
3285
|
return;
|
|
3286
3286
|
}
|
|
3287
3287
|
const envFile = `${paths(cfg).app}/env.${svc.name}`;
|
|
@@ -3360,12 +3360,12 @@ Options
|
|
|
3360
3360
|
const hosts = names.map((h) => host(h, cfg.ssh));
|
|
3361
3361
|
switch (sub) {
|
|
3362
3362
|
case "status": {
|
|
3363
|
-
for (const
|
|
3364
|
-
const s = await proxyState(
|
|
3365
|
-
const listening = cfg.mode === "docker" ? await
|
|
3363
|
+
for (const host of hosts) {
|
|
3364
|
+
const s = await proxyState(host, cfg);
|
|
3365
|
+
const listening = cfg.mode === "docker" ? await host.capture(`docker port ${PROXY_CONTAINER} 2>/dev/null | sort -u || true`, { allowFailure: true }) : Object.entries(cfg.proxy.entrypoints).map(([n, p]) => `${n} -> :${p}`).join(`
|
|
3366
3366
|
`);
|
|
3367
|
-
const apps = await
|
|
3368
|
-
log.plain(`${c.magenta(
|
|
3367
|
+
const apps = await host.capture(`ls -1 ${shq(paths(cfg).proxyDynamicDir)} 2>/dev/null | sed 's/\\.yml$//' || true`, { allowFailure: true });
|
|
3368
|
+
log.plain(`${c.magenta(host.name)} ${s.running ? c.green("running") : c.red(s.status)} ${c.gray(s.image)}`);
|
|
3369
3369
|
if (listening)
|
|
3370
3370
|
log.plain(listening.split(`
|
|
3371
3371
|
`).map((l) => ` ${l}`).join(`
|
|
@@ -3377,8 +3377,8 @@ Options
|
|
|
3377
3377
|
return;
|
|
3378
3378
|
}
|
|
3379
3379
|
case "reboot": {
|
|
3380
|
-
await pool(hosts, 4, async (
|
|
3381
|
-
await ensureProxy(
|
|
3380
|
+
await pool(hosts, 4, async (host) => {
|
|
3381
|
+
await ensureProxy(host, cfg, { force: true, secrets: loaded.env });
|
|
3382
3382
|
});
|
|
3383
3383
|
log.ok("proxy restarted");
|
|
3384
3384
|
return;
|
|
@@ -3386,40 +3386,40 @@ Options
|
|
|
3386
3386
|
case "logs": {
|
|
3387
3387
|
const lines = String(ctx.values.lines ?? "100");
|
|
3388
3388
|
const follow = Boolean(ctx.values.follow);
|
|
3389
|
-
for (const
|
|
3389
|
+
for (const host of hosts) {
|
|
3390
3390
|
const cmd = cfg.mode === "docker" ? `docker logs ${follow ? "-f " : ""}--tail ${lines} ${PROXY_CONTAINER} 2>&1` : `${pathPrelude(cfg)} pm2 logs ${PROXY_PROCESS} --lines ${lines}${follow ? "" : " --nostream"} 2>&1`;
|
|
3391
3391
|
if (follow) {
|
|
3392
|
-
log.info(`following proxy on ${
|
|
3393
|
-
await
|
|
3392
|
+
log.info(`following proxy on ${host.name} \u2014 Ctrl-C to stop`);
|
|
3393
|
+
await host.interactive(cmd, { sudo: cfg.mode === "docker" ? undefined : false });
|
|
3394
3394
|
} else {
|
|
3395
|
-
const out = await
|
|
3396
|
-
log.plain(c.magenta(
|
|
3395
|
+
const out = await host.exec(`${cmd} || true`, { allowFailure: true, sudo: cfg.mode === "docker" ? undefined : false });
|
|
3396
|
+
log.plain(c.magenta(host.name));
|
|
3397
3397
|
log.plain(out.stdout || c.gray(" no output"));
|
|
3398
3398
|
}
|
|
3399
3399
|
}
|
|
3400
3400
|
return;
|
|
3401
3401
|
}
|
|
3402
3402
|
case "routes": {
|
|
3403
|
-
for (const
|
|
3404
|
-
const doc = await
|
|
3405
|
-
log.plain(c.magenta(
|
|
3403
|
+
for (const host of hosts) {
|
|
3404
|
+
const doc = await host.readFile(paths(cfg).proxyDynamic);
|
|
3405
|
+
log.plain(c.magenta(host.name));
|
|
3406
3406
|
log.plain(doc ?? c.gray(" no routes written yet"));
|
|
3407
3407
|
}
|
|
3408
3408
|
return;
|
|
3409
3409
|
}
|
|
3410
3410
|
case "remove": {
|
|
3411
|
-
for (const
|
|
3412
|
-
await removeRoutes(
|
|
3413
|
-
const left = await
|
|
3411
|
+
for (const host of hosts) {
|
|
3412
|
+
await removeRoutes(host, cfg);
|
|
3413
|
+
const left = await host.capture(`ls -1 ${shq(paths(cfg).proxyDynamicDir)}/*.yml 2>/dev/null | wc -l`, { allowFailure: true });
|
|
3414
3414
|
if (Number(left.trim()) === 0) {
|
|
3415
3415
|
if (cfg.mode === "docker") {
|
|
3416
|
-
await
|
|
3416
|
+
await host.exec(`docker rm -f ${PROXY_CONTAINER} >/dev/null 2>&1 || true`, { allowFailure: true });
|
|
3417
3417
|
} else {
|
|
3418
|
-
await
|
|
3418
|
+
await host.exec(`${pathPrelude(cfg)} pm2 delete ${PROXY_PROCESS} >/dev/null 2>&1 || true`, { allowFailure: true, sudo: false });
|
|
3419
3419
|
}
|
|
3420
|
-
log.host(
|
|
3420
|
+
log.host(host.name, "routes removed, proxy stopped (no apps left)");
|
|
3421
3421
|
} else {
|
|
3422
|
-
log.host(
|
|
3422
|
+
log.host(host.name, `routes removed, proxy still serving ${left.trim()} other app(s)`);
|
|
3423
3423
|
}
|
|
3424
3424
|
}
|
|
3425
3425
|
return;
|
|
@@ -3560,8 +3560,8 @@ If a deploy was killed halfway, release it by hand.`,
|
|
|
3560
3560
|
}
|
|
3561
3561
|
if (sub !== "status")
|
|
3562
3562
|
throw new EpdError(`unknown subcommand "${sub}"`, "Try: status, release");
|
|
3563
|
-
for (const { host
|
|
3564
|
-
log.plain(info ? `${c.magenta(
|
|
3563
|
+
for (const { host, info } of await DeployLock.status(cfg, hosts)) {
|
|
3564
|
+
log.plain(info ? `${c.magenta(host)} ${c.yellow("locked")} by ${info.by} ${c.gray(`${info.at} \u2014 ${info.what}`)}` : `${c.magenta(host)} ${c.green("free")}`);
|
|
3565
3565
|
}
|
|
3566
3566
|
}
|
|
3567
3567
|
};
|
|
@@ -3593,16 +3593,16 @@ Options
|
|
|
3593
3593
|
return;
|
|
3594
3594
|
}
|
|
3595
3595
|
}
|
|
3596
|
-
await pool(names.map((h) => host(h, cfg.ssh)), 4, async (
|
|
3597
|
-
await removeRoutes(
|
|
3596
|
+
await pool(names.map((h) => host(h, cfg.ssh)), 4, async (host) => {
|
|
3597
|
+
await removeRoutes(host, cfg);
|
|
3598
3598
|
if (cfg.mode === "docker")
|
|
3599
|
-
await removeApp(
|
|
3599
|
+
await removeApp(host, cfg);
|
|
3600
3600
|
else
|
|
3601
|
-
await removeApp2(
|
|
3601
|
+
await removeApp2(host, cfg);
|
|
3602
3602
|
if (!ctx.values["keep-data"]) {
|
|
3603
|
-
await
|
|
3603
|
+
await host.exec(`rm -rf ${shq(paths(cfg).app)} ${shq(`${paths(cfg).portsDir}/${cfg.portBase}`)}`, { allowFailure: true });
|
|
3604
3604
|
}
|
|
3605
|
-
log.host(
|
|
3605
|
+
log.host(host.name, "removed");
|
|
3606
3606
|
});
|
|
3607
3607
|
log.ok(`${cfg.name} removed from ${names.length} host(s)`);
|
|
3608
3608
|
log.info(`the shared proxy is still running \u2014 ${c.cyan("epd proxy remove")} stops it if nothing else uses it`);
|
|
@@ -3755,7 +3755,7 @@ var commands = [
|
|
|
3755
3755
|
];
|
|
3756
3756
|
|
|
3757
3757
|
// src/cli.ts
|
|
3758
|
-
var VERSION = "0.1.
|
|
3758
|
+
var VERSION = "0.1.1";
|
|
3759
3759
|
var USAGE = `${c.bold("epd")} \u2014 Easy Project Deployer ${c.gray(`v${VERSION}`)}
|
|
3760
3760
|
|
|
3761
3761
|
${c.bold("Usage")}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabioplunser/epd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Easy Project Deployer — deploy any project to one or many servers with Docker or PM2, fronted by Traefik.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Fabio Plünser",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"homepage": "https://github.com/FabioPlunser/epd#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "
|
|
11
|
+
"url": "https://github.com/FabioPlunser/epd"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/FabioPlunser/epd/issues"
|