@norskvideo/ctl-dev-kit 0.2.75 → 0.2.76
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/conventions/check-drift.ts +33 -0
- package/conventions/server-install.sh +291 -0
- package/conventions/server-uninstall.sh +182 -0
- package/conventions/sync-drift.ts +17 -0
- package/create-product/canon.ts +6 -0
- package/create-product/server-kit.ts +95 -0
- package/create-product/turnkey.ts +18 -0
- package/package.json +1 -1
|
@@ -292,6 +292,12 @@ export interface CanonicalBytes {
|
|
|
292
292
|
integration: string;
|
|
293
293
|
/** conventions/demo.sh — the scripts/demo shim a product with tests/demo.spec.ts carries. */
|
|
294
294
|
demoShim: string;
|
|
295
|
+
/** conventions/server-install.sh and server-uninstall.sh — the server
|
|
296
|
+
* deployment kit's deployment/install.sh and uninstall.sh. Gated only for a
|
|
297
|
+
* copy that names its canonical source, so a product's own hand-written
|
|
298
|
+
* install.sh at that path is left to it. */
|
|
299
|
+
serverInstall?: string;
|
|
300
|
+
serverUninstall?: string;
|
|
295
301
|
/** doc-guide/with-display.sh — the X-display wrapper BOTH docs workflows invoke
|
|
296
302
|
* as `scripts/with-display.sh`. Gated because the workflow is: an ungated
|
|
297
303
|
* hand copy is how build-docs.yml came to fail in both turnkeys, which never
|
|
@@ -312,6 +318,30 @@ export interface CanonicalBytes {
|
|
|
312
318
|
bumpSdkApp?: string;
|
|
313
319
|
}
|
|
314
320
|
|
|
321
|
+
export const SERVER_KIT_COPIES = [
|
|
322
|
+
{ rel: "deployment/install.sh", source: "server-install.sh", key: "serverInstall" },
|
|
323
|
+
{ rel: "deployment/uninstall.sh", source: "server-uninstall.sh", key: "serverUninstall" },
|
|
324
|
+
] as const;
|
|
325
|
+
|
|
326
|
+
/** A copy names its source in its header; a product's own script does not. */
|
|
327
|
+
export const isServerKitCopy = (content: string, source: string): boolean =>
|
|
328
|
+
content.includes(`@norskvideo/ctl-dev-kit conventions/${source}`);
|
|
329
|
+
|
|
330
|
+
export function serverKitProblems(repoRoot: string, canonical: CanonicalBytes): string[] {
|
|
331
|
+
const problems: string[] = [];
|
|
332
|
+
for (const { rel, source, key } of SERVER_KIT_COPIES) {
|
|
333
|
+
const expected = canonical[key];
|
|
334
|
+
const path = join(repoRoot, rel);
|
|
335
|
+
if (expected === undefined || !existsSync(path)) continue;
|
|
336
|
+
const actual = readFileSync(path, "utf8");
|
|
337
|
+
if (!isServerKitCopy(actual, source) || actual === expected) continue;
|
|
338
|
+
problems.push(
|
|
339
|
+
`${rel} has drifted from @norskvideo/ctl-dev-kit conventions/${source} (${firstDiffLine(actual, expected)}). ${RESYNC}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return problems;
|
|
343
|
+
}
|
|
344
|
+
|
|
315
345
|
// 05-demo s6: a product that has a demo (tests/demo.spec.ts) exposes it the
|
|
316
346
|
// shared way — scripts/demo, the verbatim executable shim that re-execs under
|
|
317
347
|
// the nix dev shell and delegates to `bun run demo`, and the package.json
|
|
@@ -893,6 +923,7 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
|
|
|
893
923
|
for (const p of docsSiteExposureProblems(existsSync(siteWorkflowPath), hasDocsSite(repoRoot), opts.repoName)) push(p);
|
|
894
924
|
|
|
895
925
|
for (const p of demoProblems(repoRoot, canonical.demoShim)) push(p);
|
|
926
|
+
for (const p of serverKitProblems(repoRoot, canonical)) push(p);
|
|
896
927
|
for (const p of productLogsProblems(repoRoot)) push(p);
|
|
897
928
|
for (const p of bunVersionProblems(repoRoot, canonical.ctlShell)) push(p);
|
|
898
929
|
for (const p of ctlPinProblems(repoRoot)) push(p);
|
|
@@ -922,6 +953,8 @@ if (import.meta.main) {
|
|
|
922
953
|
smoke: readFileSync(join(import.meta.dir, "smoke.yml"), "utf8"),
|
|
923
954
|
integration: readFileSync(join(import.meta.dir, "integration.yml"), "utf8"),
|
|
924
955
|
demoShim: readFileSync(join(import.meta.dir, "demo.sh"), "utf8"),
|
|
956
|
+
serverInstall: readFileSync(join(import.meta.dir, "server-install.sh"), "utf8"),
|
|
957
|
+
serverUninstall: readFileSync(join(import.meta.dir, "server-uninstall.sh"), "utf8"),
|
|
925
958
|
flakeSdkApp: readFileSync(join(import.meta.dir, "..", "build", "flake.sdk-app.nix"), "utf8"),
|
|
926
959
|
checksSdkApp: readFileSync(join(import.meta.dir, "checks.sdk-app.yml"), "utf8"),
|
|
927
960
|
bumpSdkApp: readFileSync(join(import.meta.dir, "bump.sdk-app.yml"), "utf8"),
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install this product on a server, from a release directory written by
|
|
3
|
+
# deployment/package-release.sh. Takes a box with nothing of ours on it to a
|
|
4
|
+
# running instance; uninstall.sh (beside it) takes it back.
|
|
5
|
+
#
|
|
6
|
+
# Runs ON the server, as a sudo-capable user, from the release directory:
|
|
7
|
+
#
|
|
8
|
+
# export NORSK_ADMIN_PASSWORD=... # first install only: the web login (>=8 chars, >=1 digit)
|
|
9
|
+
# export <PARAMETER>=... # any launch parameter the template declares
|
|
10
|
+
# bash install.sh --license /path/to/licence.json
|
|
11
|
+
#
|
|
12
|
+
# Launch parameters are the product template's own: every parameter it declares
|
|
13
|
+
# is passed when a variable of that name is set, a required one must be, and
|
|
14
|
+
# the rest take the template's defaults. Secrets come from the environment,
|
|
15
|
+
# never the command line.
|
|
16
|
+
#
|
|
17
|
+
# Idempotent: each step checks before it acts, so a re-run after a failure picks
|
|
18
|
+
# up where it stopped, and a re-run with a newer release replaces the product (a
|
|
19
|
+
# stored template never sees a new image otherwise) and relaunches the instance
|
|
20
|
+
# in the same working directory, so its media and settings survive.
|
|
21
|
+
#
|
|
22
|
+
# Access (ACCESS, default tunnel): the web page stays off the public network and
|
|
23
|
+
# is reached over SSH, the shape a host that admits only SSH and media needs:
|
|
24
|
+
# ssh -L 8443:127.0.0.1:8443 <user>@<host> # then https://localhost:8443
|
|
25
|
+
# The proxy listens on PROXY_PORT (8443), the port the tunnel forwards, so the
|
|
26
|
+
# URLs norsk-ctl advertises are the ones the browser uses. ACCESS=public serves
|
|
27
|
+
# 443 to the world instead, at PUBLIC_HOST (default the detected public IP).
|
|
28
|
+
#
|
|
29
|
+
# Options (environment):
|
|
30
|
+
# INSTANCE_ID default: the product's name
|
|
31
|
+
# CERT_SOURCE, DOMAIN, CERT_EMAIL passed to the norsk-ctl installer
|
|
32
|
+
# RELAUNCH=1 replace a running instance, e.g. to apply
|
|
33
|
+
# a changed parameter
|
|
34
|
+
#
|
|
35
|
+
# Copied verbatim from @norskvideo/ctl-dev-kit conventions/server-install.sh and
|
|
36
|
+
# drift-gated: edit the dev-kit source and re-sync, never this copy.
|
|
37
|
+
set -euo pipefail
|
|
38
|
+
|
|
39
|
+
CTL=(sudo -u norsk env NORSK_CTL_CONFIG_DIR=/etc/norsk-ctl NORSK_CTL_STATE_DIR=/var/lib/norsk-ctl
|
|
40
|
+
NORSK_CTL_LOG_DIR=/var/log/norsk-ctl norsk-ctl)
|
|
41
|
+
CTL_WAIT_SECONDS="${CTL_WAIT_SECONDS:-90}"
|
|
42
|
+
|
|
43
|
+
info() { printf '\n==> %s\n' "$*"; }
|
|
44
|
+
oops() {
|
|
45
|
+
printf 'error: %s\n' "$*" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
}
|
|
48
|
+
# As the service user with its configuration: the installer adds us to the
|
|
49
|
+
# norsk group, but not in this login session, and sudo drops the profile.d
|
|
50
|
+
# environment the CLI reads.
|
|
51
|
+
ctl() { "${CTL[@]}" "$@"; }
|
|
52
|
+
|
|
53
|
+
# Bounded by the clock, not by attempts: a CLI call against a daemon that is
|
|
54
|
+
# down can itself take many seconds.
|
|
55
|
+
wait_for_ctl() {
|
|
56
|
+
local deadline=$((SECONDS + CTL_WAIT_SECONDS))
|
|
57
|
+
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
58
|
+
timeout 5 "${CTL[@]}" product list >/dev/null 2>&1 && return 0
|
|
59
|
+
sleep 1
|
|
60
|
+
done
|
|
61
|
+
oops "norsk-ctl did not come back within ${CTL_WAIT_SECONDS}s (journalctl -u norsk-ctl)"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Appends `key: value` to a YAML file unless the key is set. On its own line
|
|
65
|
+
# whether or not the file ends in a newline: norsk-ctl's init writes
|
|
66
|
+
# config.yaml without one, and a key joined to the last line stops the daemon.
|
|
67
|
+
ensure_config_line() {
|
|
68
|
+
local key=$1 value=$2 file=$3
|
|
69
|
+
sudo grep -q "^${key}:" "${file}" && return 1
|
|
70
|
+
printf '\n%s: %s\n' "${key}" "${value}" | sudo tee -a "${file}" >/dev/null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# The template's parameters (`template show -o json` on stdin) as --param
|
|
74
|
+
# arguments, NUL-separated: a parameter is passed when a variable of its name is
|
|
75
|
+
# set and non-empty; INSTANCE_NAME defaults to the instance id. Fails naming
|
|
76
|
+
# every required parameter that is unset.
|
|
77
|
+
launch_params() {
|
|
78
|
+
local name required value missing=()
|
|
79
|
+
while IFS=$'\t' read -r name required; do
|
|
80
|
+
value="${!name:-}"
|
|
81
|
+
[ -z "${value}" ] && [ "${name}" = INSTANCE_NAME ] && value="${INSTANCE_ID}"
|
|
82
|
+
if [ -n "${value}" ]; then
|
|
83
|
+
printf -- '--param\0%s=%s\0' "${name}" "${value}"
|
|
84
|
+
elif [ "${required}" = true ]; then
|
|
85
|
+
missing+=("${name}")
|
|
86
|
+
fi
|
|
87
|
+
done < <(jq -r '.parameters[] | [.name, (.required // false | tostring)] | @tsv')
|
|
88
|
+
[ "${#missing[@]}" = 0 ] || oops "export the required launch parameters: ${missing[*]}"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main() {
|
|
92
|
+
local here
|
|
93
|
+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
94
|
+
# shellcheck source=/dev/null
|
|
95
|
+
. "${here}/release.env" # PRODUCT, IMAGE, IMAGE_ARCH, IMAGE_TAR, CTL_VERSION, RELEASE_COMMIT
|
|
96
|
+
|
|
97
|
+
local license=""
|
|
98
|
+
while [ $# -gt 0 ]; do
|
|
99
|
+
case "$1" in
|
|
100
|
+
--license)
|
|
101
|
+
license=$2
|
|
102
|
+
shift
|
|
103
|
+
;;
|
|
104
|
+
*)
|
|
105
|
+
echo "unknown argument $1" >&2
|
|
106
|
+
exit 2
|
|
107
|
+
;;
|
|
108
|
+
esac
|
|
109
|
+
shift
|
|
110
|
+
done
|
|
111
|
+
INSTANCE_ID="${INSTANCE_ID:-${PRODUCT}}"
|
|
112
|
+
local access="${ACCESS:-tunnel}" proxy_port="${PROXY_PORT:-8443}"
|
|
113
|
+
case "${access}" in
|
|
114
|
+
tunnel | public) ;;
|
|
115
|
+
*)
|
|
116
|
+
echo "ACCESS must be tunnel or public, not ${access}" >&2
|
|
117
|
+
exit 2
|
|
118
|
+
;;
|
|
119
|
+
esac
|
|
120
|
+
local record_dir="/var/lib/${PRODUCT}-install"
|
|
121
|
+
local record="${record_dir}/record.env"
|
|
122
|
+
|
|
123
|
+
info "Preflight"
|
|
124
|
+
[ -n "${license}" ] || oops "missing --license"
|
|
125
|
+
[ -r "${license}" ] || oops "cannot read ${license}"
|
|
126
|
+
# Docker is the box's own: the norsk-ctl installer would add it when missing,
|
|
127
|
+
# and uninstall.sh could then not tell whose it was.
|
|
128
|
+
for tool in curl jq docker tar gzip; do
|
|
129
|
+
command -v "${tool}" >/dev/null || oops "${tool} is required"
|
|
130
|
+
done
|
|
131
|
+
sudo -n true 2>/dev/null || sudo -v
|
|
132
|
+
local arch
|
|
133
|
+
case "$(uname -m)" in
|
|
134
|
+
x86_64) arch=amd64 ;;
|
|
135
|
+
aarch64 | arm64) arch=arm64 ;;
|
|
136
|
+
*) arch=unknown ;;
|
|
137
|
+
esac
|
|
138
|
+
[ "${arch}" = "${IMAGE_ARCH}" ] || oops "this release is ${IMAGE_ARCH}, this server is ${arch}"
|
|
139
|
+
|
|
140
|
+
# What the box looked like before us, so uninstall.sh restores exactly that.
|
|
141
|
+
# Written once: a re-run must not record our own changes as the baseline.
|
|
142
|
+
if ! sudo test -f "${record}"; then
|
|
143
|
+
info "Recording the baseline (${record})"
|
|
144
|
+
local invoking_user="${SUDO_USER:-$(id -un)}"
|
|
145
|
+
sudo install -d -m 0755 "${record_dir}"
|
|
146
|
+
{
|
|
147
|
+
printf 'RECORDED_AT=%q\n' "$(date -Is)"
|
|
148
|
+
printf 'INVOKING_USER=%q\n' "${invoking_user}"
|
|
149
|
+
printf 'USER_GROUPS_BEFORE=%q\n' "$(id -nG "${invoking_user}")"
|
|
150
|
+
printf 'CTL_PREINSTALLED=%q\n' "$(command -v norsk-ctl >/dev/null && echo 1 || echo 0)"
|
|
151
|
+
printf 'IMAGES_BEFORE=%q\n' "$(sudo docker image ls -q --no-trunc | sort -u | tr '\n' ' ')"
|
|
152
|
+
for key in net.core.rmem_max net.core.wmem_max net.core.rmem_default net.core.wmem_default \
|
|
153
|
+
net.core.netdev_max_backlog net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.ipv4.tcp_congestion_control; do
|
|
154
|
+
printf 'SYSCTL_%s=%q\n' "$(echo "${key}" | tr '.' '_')" "$(sysctl -n "${key}" | tr '\t' ' ')"
|
|
155
|
+
done
|
|
156
|
+
} | sudo tee "${record}" >/dev/null
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
if ! command -v norsk-ctl >/dev/null; then
|
|
160
|
+
info "norsk-ctl ${CTL_VERSION} (public installer)"
|
|
161
|
+
: "${NORSK_ADMIN_PASSWORD:?export NORSK_ADMIN_PASSWORD (>=8 chars, >=1 digit)}"
|
|
162
|
+
export NORSK_ADMIN_PASSWORD
|
|
163
|
+
local installer
|
|
164
|
+
installer="$(mktemp)"
|
|
165
|
+
curl -fsSL https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl/install.sh -o "${installer}"
|
|
166
|
+
local flags=()
|
|
167
|
+
[ -n "${CERT_SOURCE:-}" ] && flags+=(--cert-source "${CERT_SOURCE}")
|
|
168
|
+
[ -n "${DOMAIN:-}" ] && flags+=(--domain "${DOMAIN}")
|
|
169
|
+
[ -n "${CERT_EMAIL:-}" ] && flags+=(--cert-email "${CERT_EMAIL}")
|
|
170
|
+
if [ "${access}" = tunnel ]; then
|
|
171
|
+
flags+=(--public-host localhost --proxy-port "${proxy_port}" --no-http-redirect)
|
|
172
|
+
if grep -q -- '--bind-address)' "${installer}"; then
|
|
173
|
+
flags+=(--bind-address 127.0.0.1)
|
|
174
|
+
else
|
|
175
|
+
echo "warning: this norsk-ctl cannot bind to loopback; its proxy and API listen on every interface" >&2
|
|
176
|
+
fi
|
|
177
|
+
else
|
|
178
|
+
flags+=(--public-host "${PUBLIC_HOST:-auto}")
|
|
179
|
+
fi
|
|
180
|
+
bash "${installer}" --server --version "${CTL_VERSION}" --license "${license}" "${flags[@]}" --pull-images --yes
|
|
181
|
+
rm -f "${installer}"
|
|
182
|
+
fi
|
|
183
|
+
# The public installer leaves a running norsk-ctl as it is, so a box set up by
|
|
184
|
+
# an earlier release is upgraded here, as norsk-ctl's upgrade guide does it:
|
|
185
|
+
# back up its config and state, swap the binary, restart the service. The
|
|
186
|
+
# daemon's SIGTERM stops only itself; the proxy and instances keep running.
|
|
187
|
+
local installed_ctl
|
|
188
|
+
installed_ctl="$(norsk-ctl --version)"
|
|
189
|
+
if [ "${installed_ctl}" != "${CTL_VERSION}" ]; then
|
|
190
|
+
info "Upgrading norsk-ctl ${installed_ctl} -> ${CTL_VERSION}"
|
|
191
|
+
local plat base bin want backup
|
|
192
|
+
plat="linux-$([ "${arch}" = amd64 ] && echo x64 || echo arm64)"
|
|
193
|
+
base="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl/${CTL_VERSION}"
|
|
194
|
+
bin="$(mktemp)"
|
|
195
|
+
curl -fsSL "${base}/norsk-ctl-${CTL_VERSION}-${plat}" -o "${bin}"
|
|
196
|
+
want="$(curl -fsSL "${base}/norsk-ctl-${CTL_VERSION}-${plat}.sha256" | awk '{print $1}')"
|
|
197
|
+
[ "$(sha256sum "${bin}" | awk '{print $1}')" = "${want}" ] || oops "norsk-ctl ${CTL_VERSION} failed its checksum"
|
|
198
|
+
backup="${record_dir}/norsk-ctl-${installed_ctl}.tar.gz"
|
|
199
|
+
sudo tar -czf "${backup}" -C / etc/norsk-ctl var/lib/norsk-ctl
|
|
200
|
+
echo "backed up norsk-ctl's config and state to ${backup}"
|
|
201
|
+
sudo install -o norsk -g norsk -m 0755 "${bin}" "/opt/norsk-ctl/bin/norsk-ctl-${CTL_VERSION}"
|
|
202
|
+
rm -f "${bin}"
|
|
203
|
+
sudo ln -sfn "/opt/norsk-ctl/bin/norsk-ctl-${CTL_VERSION}" /usr/local/bin/norsk-ctl
|
|
204
|
+
sudo systemctl restart norsk-ctl
|
|
205
|
+
wait_for_ctl
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Behind a tunnel, norsk-ctl's shared MoQ relay would advertise its preview
|
|
209
|
+
# URLs from the tunnel's localhost, where no browser can reach them over UDP.
|
|
210
|
+
# An instance's own preview listener, given the box's public address, can.
|
|
211
|
+
local relay_dropin="/etc/systemd/system/norsk-ctl.service.d/${PRODUCT}.conf"
|
|
212
|
+
if [ "${access}" = tunnel ] && ! sudo test -f "${relay_dropin}"; then
|
|
213
|
+
info "Turning off norsk-ctl's shared MoQ relay"
|
|
214
|
+
sudo install -d /etc/systemd/system/norsk-ctl.service.d
|
|
215
|
+
printf '[Service]\nEnvironment=NORSK_CTL_MOQ_RELAY_ENABLE=0\n' | sudo tee "${relay_dropin}" >/dev/null
|
|
216
|
+
sudo systemctl daemon-reload
|
|
217
|
+
sudo systemctl restart norsk-ctl
|
|
218
|
+
wait_for_ctl
|
|
219
|
+
sudo docker rm -f norsk-moq-relay >/dev/null 2>&1 || true
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# Behind a tunnel the proxy and the daemon's API stay on loopback, on a
|
|
223
|
+
# norsk-ctl that can say so. An install made before it could gets the setting
|
|
224
|
+
# here; the restart republishes the proxy on it.
|
|
225
|
+
if [ "${access}" = tunnel ] && norsk-ctl init --help 2>/dev/null | grep -q -- '--bind-address' &&
|
|
226
|
+
ensure_config_line bindAddress 127.0.0.1 /etc/norsk-ctl/config.yaml; then
|
|
227
|
+
info "Bound the proxy and the daemon's API to loopback"
|
|
228
|
+
sudo systemctl restart norsk-ctl
|
|
229
|
+
wait_for_ctl
|
|
230
|
+
fi
|
|
231
|
+
local staged_license
|
|
232
|
+
staged_license="/etc/norsk-ctl/licenses/$(basename "${license}")"
|
|
233
|
+
sudo test -f "${staged_license}" || sudo install -m 0640 -o norsk -g norsk "${license}" "${staged_license}"
|
|
234
|
+
|
|
235
|
+
info "Product image ${IMAGE}"
|
|
236
|
+
sudo docker image inspect "${IMAGE}" >/dev/null 2>&1 || gunzip -c "${here}/${IMAGE_TAR}" | sudo docker load
|
|
237
|
+
|
|
238
|
+
info "Product ${PRODUCT}"
|
|
239
|
+
local registered
|
|
240
|
+
registered="$(ctl product list -o json |
|
|
241
|
+
jq -r --arg p "${PRODUCT}" '[.. | objects | select(has("spec") and .name == $p) | .spec.image][0] // ""')"
|
|
242
|
+
if [ -n "${registered}" ] && [ "${registered}" != "${IMAGE}" ]; then
|
|
243
|
+
# A stored template is fetched once at `product add`; a new image reaches
|
|
244
|
+
# nothing until the product is added again.
|
|
245
|
+
info "Replacing ${PRODUCT} (${registered} -> ${IMAGE})"
|
|
246
|
+
# Not purged: the relaunch below takes the same working directory.
|
|
247
|
+
if ctl instance exists "${INSTANCE_ID}" >/dev/null 2>&1; then ctl instance delete "${INSTANCE_ID}"; fi
|
|
248
|
+
for t in $(ctl template list -o json |
|
|
249
|
+
jq -r --arg p "${PRODUCT}" '.productTemplates[] | select(.source.productName == $p) | .name'); do
|
|
250
|
+
ctl template remove "${t}"
|
|
251
|
+
done
|
|
252
|
+
ctl product remove "${PRODUCT}"
|
|
253
|
+
registered=""
|
|
254
|
+
fi
|
|
255
|
+
[ -n "${registered}" ] || ctl product add --license-file "${staged_license}" --image "${IMAGE}"
|
|
256
|
+
|
|
257
|
+
info "Instance ${INSTANCE_ID}"
|
|
258
|
+
local template
|
|
259
|
+
template="$(ctl template list -o json |
|
|
260
|
+
jq -r --arg p "${PRODUCT}" '.productTemplates[] | select(.source.kind == "product-default" and .source.productName == $p) | .name' |
|
|
261
|
+
head -1)"
|
|
262
|
+
[ -n "${template}" ] || oops "no product-default template for ${PRODUCT}"
|
|
263
|
+
# Through files, not a pipe: launch_params then runs in this shell, and a
|
|
264
|
+
# missing required parameter stops the install before the instance is touched.
|
|
265
|
+
local shown params=()
|
|
266
|
+
shown="$(mktemp)"
|
|
267
|
+
ctl template show "${template}" -o json >"${shown}"
|
|
268
|
+
launch_params <"${shown}" >"${shown}.args"
|
|
269
|
+
mapfile -d '' params <"${shown}.args"
|
|
270
|
+
rm -f "${shown}" "${shown}.args"
|
|
271
|
+
if [ "${RELAUNCH:-}" = 1 ] && ctl instance exists "${INSTANCE_ID}" >/dev/null 2>&1; then
|
|
272
|
+
ctl instance delete "${INSTANCE_ID}"
|
|
273
|
+
fi
|
|
274
|
+
if ! ctl instance exists "${INSTANCE_ID}" >/dev/null 2>&1; then
|
|
275
|
+
# TODO: --param puts secrets on a process argv for the length of the call;
|
|
276
|
+
# move them to an env-file once ctl takes one.
|
|
277
|
+
ctl instance launch-template "${INSTANCE_ID}" --template "${template}" "${params[@]}"
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
info "Done (release ${RELEASE_COMMIT})"
|
|
281
|
+
ctl instance list
|
|
282
|
+
echo
|
|
283
|
+
if [ "${access}" = tunnel ]; then
|
|
284
|
+
echo "Reach it with: ssh -L ${proxy_port}:127.0.0.1:${proxy_port} $(id -un)@<this host>, then https://localhost:${proxy_port}"
|
|
285
|
+
echo "Open inbound only the product's media ports (docs/administering.md lists them)."
|
|
286
|
+
else
|
|
287
|
+
echo "Open inbound: 443/tcp and the product's media ports (docs/administering.md lists them)."
|
|
288
|
+
fi
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if [ "${BASH_SOURCE[0]}" = "$0" ]; then main "$@"; fi
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Take a server back to how it was before install.sh: the product, its
|
|
3
|
+
# instances and images, then norsk-ctl itself with everything its installer put
|
|
4
|
+
# on the box. Docker and anything else the box already had stay.
|
|
5
|
+
#
|
|
6
|
+
# bash uninstall.sh [--keep-ctl | --remove-ctl] [--yes]
|
|
7
|
+
#
|
|
8
|
+
# Runs from the release directory (release.env names the product). norsk-ctl is
|
|
9
|
+
# removed only when install.sh installed it (its baseline record says so) or no
|
|
10
|
+
# record exists, and never while another product is still registered.
|
|
11
|
+
# --keep-ctl removes only this product; --remove-ctl removes norsk-ctl even
|
|
12
|
+
# though it was there before install.sh.
|
|
13
|
+
#
|
|
14
|
+
# Ends by listing anything left behind, and exits non-zero if there is any.
|
|
15
|
+
#
|
|
16
|
+
# Copied verbatim from @norskvideo/ctl-dev-kit conventions/server-uninstall.sh
|
|
17
|
+
# and drift-gated: edit the dev-kit source and re-sync, never this copy.
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
info() { printf '\n==> %s\n' "$*"; }
|
|
21
|
+
oops() {
|
|
22
|
+
printf 'error: %s\n' "$*" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
}
|
|
25
|
+
ctl() {
|
|
26
|
+
sudo -u norsk env NORSK_CTL_CONFIG_DIR=/etc/norsk-ctl NORSK_CTL_STATE_DIR=/var/lib/norsk-ctl \
|
|
27
|
+
NORSK_CTL_LOG_DIR=/var/log/norsk-ctl norsk-ctl "$@"
|
|
28
|
+
}
|
|
29
|
+
have_ctl() { command -v norsk-ctl >/dev/null && systemctl is-active --quiet norsk-ctl; }
|
|
30
|
+
|
|
31
|
+
# Whether ctl goes too: asked to keep it, or it predates install.sh and nobody
|
|
32
|
+
# asked for it gone.
|
|
33
|
+
removes_ctl() {
|
|
34
|
+
local keep=$1 remove=$2 preinstalled=$3
|
|
35
|
+
[ "${keep}" = 1 ] && return 1
|
|
36
|
+
[ "${preinstalled}" = 1 ] && [ "${remove}" != 1 ] && return 1
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main() {
|
|
41
|
+
local here
|
|
42
|
+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
43
|
+
# shellcheck source=/dev/null
|
|
44
|
+
. "${here}/release.env" # PRODUCT, IMAGE
|
|
45
|
+
local keep_ctl=0 remove_ctl_flag=0 assume_yes=0
|
|
46
|
+
while [ $# -gt 0 ]; do
|
|
47
|
+
case "$1" in
|
|
48
|
+
--keep-ctl) keep_ctl=1 ;;
|
|
49
|
+
--remove-ctl) remove_ctl_flag=1 ;;
|
|
50
|
+
--yes | -y) assume_yes=1 ;;
|
|
51
|
+
*)
|
|
52
|
+
echo "unknown argument $1" >&2
|
|
53
|
+
exit 2
|
|
54
|
+
;;
|
|
55
|
+
esac
|
|
56
|
+
shift
|
|
57
|
+
done
|
|
58
|
+
local record_dir="/var/lib/${PRODUCT}-install"
|
|
59
|
+
local record="${record_dir}/record.env"
|
|
60
|
+
|
|
61
|
+
sudo -n true 2>/dev/null || sudo -v
|
|
62
|
+
if sudo test -f "${record}"; then
|
|
63
|
+
# shellcheck source=/dev/null
|
|
64
|
+
. <(sudo cat "${record}")
|
|
65
|
+
else
|
|
66
|
+
echo "no baseline record (${record}): restoring stock defaults where the record would have said" >&2
|
|
67
|
+
fi
|
|
68
|
+
local remove_ctl=0
|
|
69
|
+
removes_ctl "${keep_ctl}" "${remove_ctl_flag}" "${CTL_PREINSTALLED:-0}" && remove_ctl=1
|
|
70
|
+
|
|
71
|
+
if [ "${assume_yes}" != 1 ]; then
|
|
72
|
+
printf 'Remove %s%s from this server? [y/N] ' "${PRODUCT}" "$([ "${remove_ctl}" = 1 ] && echo ' and norsk-ctl')"
|
|
73
|
+
local answer
|
|
74
|
+
read -r answer
|
|
75
|
+
[ "${answer}" = y ] || [ "${answer}" = Y ] || exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
if have_ctl; then
|
|
79
|
+
info "Product ${PRODUCT}"
|
|
80
|
+
for id in $(ctl instance list -o json | jq -r --arg p "${PRODUCT}" '.instances[] | select(.productName == $p) | .id'); do
|
|
81
|
+
ctl instance delete "${id}" --purge
|
|
82
|
+
done
|
|
83
|
+
for t in $(ctl template list -o json |
|
|
84
|
+
jq -r --arg p "${PRODUCT}" '.productTemplates[] | select(.source.productName == $p) | .name'); do
|
|
85
|
+
ctl template remove "${t}"
|
|
86
|
+
done
|
|
87
|
+
if ctl product exists "${PRODUCT}" >/dev/null 2>&1; then ctl product remove "${PRODUCT}"; fi
|
|
88
|
+
fi
|
|
89
|
+
sudo docker rm -f "norsk-product-${PRODUCT}" >/dev/null 2>&1 || true
|
|
90
|
+
|
|
91
|
+
local proxy_images=""
|
|
92
|
+
if [ "${remove_ctl}" = 1 ]; then
|
|
93
|
+
if have_ctl; then
|
|
94
|
+
local others
|
|
95
|
+
others="$(ctl product list -o json | jq -r '[.. | objects | select(has("spec")) | .name] | join(" ")')"
|
|
96
|
+
[ -z "${others}" ] || oops "other products are registered (${others}); re-run with --keep-ctl"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
info "norsk-ctl"
|
|
100
|
+
# The proxy's images are whatever ctl pulled; read them before the
|
|
101
|
+
# containers go.
|
|
102
|
+
proxy_images="$(sudo docker ps -a --filter name=norsk-proxy --format '{{.Image}}' | sort -u)"
|
|
103
|
+
if have_ctl; then ctl shutdown || true; fi
|
|
104
|
+
sudo systemctl disable --now norsk-ctl.service 2>/dev/null || true
|
|
105
|
+
sudo rm -f /etc/systemd/system/norsk-ctl.service
|
|
106
|
+
sudo rm -rf /etc/systemd/system/norsk-ctl.service.d
|
|
107
|
+
sudo systemctl daemon-reload
|
|
108
|
+
sudo docker rm -f norsk-proxy norsk-proxy-oauth2 norsk-moq-relay >/dev/null 2>&1 || true
|
|
109
|
+
sudo docker network rm norsk-net >/dev/null 2>&1 || true
|
|
110
|
+
|
|
111
|
+
local working_dir
|
|
112
|
+
working_dir="$(sudo awk '/^defaultWorkingDirectory:/ { print $2 }' /etc/norsk-ctl/config.yaml 2>/dev/null || true)"
|
|
113
|
+
sudo rm -rf /etc/norsk-ctl /var/lib/norsk-ctl /var/log/norsk-ctl /opt/norsk-ctl \
|
|
114
|
+
/usr/local/bin/norsk-ctl /etc/profile.d/norsk-ctl.sh "${working_dir:-/var/norsk-ctl}"
|
|
115
|
+
# TODO: certbot (--cert-source certbot): delete the certificate and the
|
|
116
|
+
# renewal timer the installer set up.
|
|
117
|
+
|
|
118
|
+
info "Kernel tuning"
|
|
119
|
+
sudo rm -f /etc/sysctl.d/90-norsk-tuning.conf
|
|
120
|
+
# Stock Ubuntu 24.04 values stand in when there is no record. Applied before
|
|
121
|
+
# `sysctl --system`, so the box's own sysctl.d settings still win.
|
|
122
|
+
restore() { sudo sysctl -q -w "$1=$2" >/dev/null || echo "warning: could not restore $1" >&2; }
|
|
123
|
+
restore net.core.rmem_max "${SYSCTL_net_core_rmem_max:-212992}"
|
|
124
|
+
restore net.core.wmem_max "${SYSCTL_net_core_wmem_max:-212992}"
|
|
125
|
+
restore net.core.rmem_default "${SYSCTL_net_core_rmem_default:-212992}"
|
|
126
|
+
restore net.core.wmem_default "${SYSCTL_net_core_wmem_default:-212992}"
|
|
127
|
+
restore net.core.netdev_max_backlog "${SYSCTL_net_core_netdev_max_backlog:-1000}"
|
|
128
|
+
restore net.ipv4.tcp_rmem "${SYSCTL_net_ipv4_tcp_rmem:-4096 131072 6291456}"
|
|
129
|
+
restore net.ipv4.tcp_wmem "${SYSCTL_net_ipv4_tcp_wmem:-4096 16384 4194304}"
|
|
130
|
+
restore net.ipv4.tcp_congestion_control "${SYSCTL_net_ipv4_tcp_congestion_control:-cubic}"
|
|
131
|
+
sudo sysctl -q --system >/dev/null
|
|
132
|
+
|
|
133
|
+
info "Service user"
|
|
134
|
+
# Deleting the norsk group also takes every member out of it. Docker
|
|
135
|
+
# membership the installer granted goes only when the record shows it was
|
|
136
|
+
# not there before.
|
|
137
|
+
if [ -n "${INVOKING_USER:-}" ] && ! grep -qw docker <<<"${USER_GROUPS_BEFORE:-docker}"; then
|
|
138
|
+
sudo gpasswd -d "${INVOKING_USER}" docker >/dev/null || true
|
|
139
|
+
fi
|
|
140
|
+
if id -u norsk >/dev/null 2>&1; then sudo userdel -r norsk 2>/dev/null || true; fi
|
|
141
|
+
if getent group norsk >/dev/null; then sudo groupdel norsk || true; fi
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
info "Images"
|
|
145
|
+
# Ours, the media and Studio images its instances ran, and the proxy's;
|
|
146
|
+
# anything the record shows was here before stays.
|
|
147
|
+
local product_repo="${IMAGE%:*}" id
|
|
148
|
+
for ref in $(sudo docker image ls --format '{{.Repository}}:{{.Tag}}' |
|
|
149
|
+
grep -E "^(${product_repo}|norskvideo/norsk|norskvideo/norsk-studio|norskvideo/norsk-moq-relay):" || true) ${proxy_images}; do
|
|
150
|
+
id="$(sudo docker image inspect -f '{{.Id}}' "${ref}" 2>/dev/null)" || continue
|
|
151
|
+
grep -qw "${id}" <<<"${IMAGES_BEFORE:-}" && continue
|
|
152
|
+
sudo docker image rm "${ref}" >/dev/null || echo "warning: could not remove ${ref}" >&2
|
|
153
|
+
done
|
|
154
|
+
|
|
155
|
+
sudo rm -rf "${record_dir}"
|
|
156
|
+
|
|
157
|
+
info "Left behind"
|
|
158
|
+
local left=0
|
|
159
|
+
check() {
|
|
160
|
+
if eval "$2"; then
|
|
161
|
+
echo " $1"
|
|
162
|
+
left=1
|
|
163
|
+
fi
|
|
164
|
+
}
|
|
165
|
+
check "image ${product_repo}" "sudo docker image ls -q '${product_repo}' | grep -q ."
|
|
166
|
+
check "container norsk-product-${PRODUCT}" "sudo docker ps -aq --filter name=norsk-product-${PRODUCT} | grep -q ."
|
|
167
|
+
if [ "${remove_ctl}" = 1 ]; then
|
|
168
|
+
for p in /etc/norsk-ctl /var/lib/norsk-ctl /var/log/norsk-ctl /opt/norsk-ctl /usr/local/bin/norsk-ctl \
|
|
169
|
+
/etc/profile.d/norsk-ctl.sh /etc/sysctl.d/90-norsk-tuning.conf /etc/systemd/system/norsk-ctl.service \
|
|
170
|
+
/etc/systemd/system/norsk-ctl.service.d; do
|
|
171
|
+
check "${p}" "sudo test -e '${p}'"
|
|
172
|
+
done
|
|
173
|
+
check "user norsk" "id -u norsk >/dev/null 2>&1"
|
|
174
|
+
check "group norsk" "getent group norsk >/dev/null"
|
|
175
|
+
check "docker network norsk-net" "sudo docker network inspect norsk-net >/dev/null 2>&1"
|
|
176
|
+
check "norsk containers" "sudo docker ps -a --format '{{.Names}}' | grep -q '^norsk-'"
|
|
177
|
+
fi
|
|
178
|
+
[ "${left}" = 0 ] && echo " nothing"
|
|
179
|
+
exit "${left}"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if [ "${BASH_SOURCE[0]}" = "$0" ]; then main "$@"; fi
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
GITIGNORE_END,
|
|
45
45
|
hasDocsSite,
|
|
46
46
|
importsCtlShell,
|
|
47
|
+
isServerKitCopy,
|
|
47
48
|
PRODUCT_LINE,
|
|
48
49
|
PRODUCT_SENTINEL,
|
|
49
50
|
type ProductShape,
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
RUNNER_LINE,
|
|
54
55
|
resolveRepoName,
|
|
55
56
|
resolveShape,
|
|
57
|
+
SERVER_KIT_COPIES,
|
|
56
58
|
splitAtMarkers,
|
|
57
59
|
} from "./check-drift.ts";
|
|
58
60
|
|
|
@@ -334,6 +336,18 @@ function syncDemoShim(repoRoot: string, canonical: string, r: SyncReport): void
|
|
|
334
336
|
chmodSync(path, 0o755);
|
|
335
337
|
}
|
|
336
338
|
|
|
339
|
+
// The server kit's scripts: only a copy (it names its source) is rewritten; a
|
|
340
|
+
// product's own install.sh at that path is the product's.
|
|
341
|
+
export function syncServerKit(repoRoot: string, canonical: CanonicalBytes, r: SyncReport): void {
|
|
342
|
+
for (const { rel, source, key } of SERVER_KIT_COPIES) {
|
|
343
|
+
const expected = canonical[key];
|
|
344
|
+
const path = join(repoRoot, rel);
|
|
345
|
+
if (expected === undefined || !existsSync(path) || !isServerKitCopy(readFileSync(path, "utf8"), source)) continue;
|
|
346
|
+
writeIfChanged(path, expected, rel, r.written);
|
|
347
|
+
chmodSync(path, 0o755);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
337
351
|
// Both docs workflows run `bash scripts/with-display.sh ...`. It stays a
|
|
338
352
|
// repo-local executable (the package.json guide scripts invoke it by that path),
|
|
339
353
|
// so a repo that carries a docs workflow gets the canonical copy written here
|
|
@@ -478,6 +492,7 @@ export function syncDrift(
|
|
|
478
492
|
syncGitignore(repoRoot, canonical.gitignoreCore, r);
|
|
479
493
|
syncDprint(repoRoot, canonical.dprint, r);
|
|
480
494
|
syncDemoShim(repoRoot, canonical.demoShim, r);
|
|
495
|
+
syncServerKit(repoRoot, canonical, r);
|
|
481
496
|
syncWithDisplay(repoRoot, canonical.withDisplay, r);
|
|
482
497
|
syncDocsTheme(repoRoot, r);
|
|
483
498
|
syncPublishDocsSite(repoRoot, canonical.publishDocsSite, opts.repoName, r);
|
|
@@ -508,6 +523,8 @@ if (import.meta.main) {
|
|
|
508
523
|
smoke: readFileSync(join(dir, "smoke.yml"), "utf8"),
|
|
509
524
|
integration: readFileSync(join(dir, "integration.yml"), "utf8"),
|
|
510
525
|
demoShim: readFileSync(join(dir, "demo.sh"), "utf8"),
|
|
526
|
+
serverInstall: readFileSync(join(dir, "server-install.sh"), "utf8"),
|
|
527
|
+
serverUninstall: readFileSync(join(dir, "server-uninstall.sh"), "utf8"),
|
|
511
528
|
flakeSdkApp: readFileSync(join(dir, "..", "build", "flake.sdk-app.nix"), "utf8"),
|
|
512
529
|
checksSdkApp: readFileSync(join(dir, "checks.sdk-app.yml"), "utf8"),
|
|
513
530
|
bumpSdkApp: readFileSync(join(dir, "bump.sdk-app.yml"), "utf8"),
|
package/create-product/canon.ts
CHANGED
|
@@ -33,6 +33,10 @@ export interface Canon {
|
|
|
33
33
|
invariantsTemplate: string;
|
|
34
34
|
/** conventions/demo.sh, emitted as scripts/demo. */
|
|
35
35
|
demoShim: string;
|
|
36
|
+
/** conventions/server-install.sh and server-uninstall.sh, emitted as
|
|
37
|
+
* deployment/install.sh and deployment/uninstall.sh. */
|
|
38
|
+
serverInstall: string;
|
|
39
|
+
serverUninstall: string;
|
|
36
40
|
/** doc-guide/with-display.sh, emitted as scripts/with-display.sh — the docs
|
|
37
41
|
* workflow invokes it by that repo-local path. */
|
|
38
42
|
withDisplay: string;
|
|
@@ -58,6 +62,8 @@ export function loadCanon(): Canon {
|
|
|
58
62
|
gitignoreCore: readFileSync(join(conventionsDir, "gitignore.core"), "utf8"),
|
|
59
63
|
invariantsTemplate: readFileSync(join(import.meta.dir, "..", "testing", "INVARIANTS.template.md"), "utf8"),
|
|
60
64
|
demoShim: readFileSync(join(conventionsDir, "demo.sh"), "utf8"),
|
|
65
|
+
serverInstall: readFileSync(join(conventionsDir, "server-install.sh"), "utf8"),
|
|
66
|
+
serverUninstall: readFileSync(join(conventionsDir, "server-uninstall.sh"), "utf8"),
|
|
61
67
|
};
|
|
62
68
|
}
|
|
63
69
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// The server deployment kit: how a turnkey reaches a customer's own server and
|
|
2
|
+
// leaves it again. install.sh and uninstall.sh are product-independent (they
|
|
3
|
+
// read the product from release.env and its launch parameters from the stored
|
|
4
|
+
// template), so they are canonical copies of conventions/server-*.sh. What is
|
|
5
|
+
// this product's is the image the release packs.
|
|
6
|
+
import type { ShapeContext } from "./create-product.ts";
|
|
7
|
+
|
|
8
|
+
export function packageReleaseSh(ctx: ShapeContext): string {
|
|
9
|
+
return `#!/usr/bin/env bash
|
|
10
|
+
# Build a release for a server: the product image as a portable tar, install.sh
|
|
11
|
+
# and uninstall.sh, and release.env naming what they install. The image is in
|
|
12
|
+
# no registry; the server pulls the media and Studio images and norsk-ctl
|
|
13
|
+
# itself.
|
|
14
|
+
#
|
|
15
|
+
# bun run package-release [-- <out-dir>] # default release/
|
|
16
|
+
# SKIP_BUILD=1 IMAGE_TAG=<repo:tag> bun run package-release # an existing image
|
|
17
|
+
#
|
|
18
|
+
# Then copy the directory to the server and run install.sh there (its header
|
|
19
|
+
# says how), e.g.
|
|
20
|
+
# tar -C release -czf - . | ssh <host> 'mkdir -p ${ctx.name}-release && tar -C ${ctx.name}-release -xzf -'
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
repo="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
|
|
24
|
+
out="\${1:-\${repo}/release}"
|
|
25
|
+
cd "\${repo}"
|
|
26
|
+
commit="$(git describe --always --dirty)"
|
|
27
|
+
# Tagged by commit: install.sh tells releases apart by tag, and a reused tag
|
|
28
|
+
# (\`:dev\`) would leave a server on the build it already has. The licence names
|
|
29
|
+
# the repository, not the tag.
|
|
30
|
+
export IMAGE_TAG="\${IMAGE_TAG:-${ctx.name}:\${commit}}"
|
|
31
|
+
[ -n "\${SKIP_BUILD:-}" ] || bun run build:image
|
|
32
|
+
# The tested norsk-ctl is the one the dev shell pins.
|
|
33
|
+
ctl_version="$(sed -n 's/^ *ctlVersion = "\\(.*\\)";/\\1/p' flake.nix)"
|
|
34
|
+
[ -n "\${ctl_version}" ] || {
|
|
35
|
+
echo "no ctlVersion in flake.nix" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
rm -rf "\${out}"
|
|
40
|
+
mkdir -p "\${out}"
|
|
41
|
+
docker save "\${IMAGE_TAG}" | gzip -1 >"\${out}/image.tar.gz"
|
|
42
|
+
cp deployment/install.sh deployment/uninstall.sh "\${out}/"
|
|
43
|
+
{
|
|
44
|
+
printf 'PRODUCT=%q\\n' ${ctx.name}
|
|
45
|
+
printf 'IMAGE=%q\\n' "\${IMAGE_TAG}"
|
|
46
|
+
printf 'IMAGE_ARCH=%q\\n' "$(docker image inspect -f '{{.Architecture}}' "\${IMAGE_TAG}")"
|
|
47
|
+
printf 'IMAGE_TAR=%q\\n' image.tar.gz
|
|
48
|
+
printf 'CTL_VERSION=%q\\n' "\${ctl_version}"
|
|
49
|
+
printf 'RELEASE_COMMIT=%q\\n' "\${commit}"
|
|
50
|
+
} >"\${out}/release.env"
|
|
51
|
+
echo "release in \${out}"
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function shipImageSh(ctx: ShapeContext): string {
|
|
56
|
+
return `#!/usr/bin/env bash
|
|
57
|
+
# Build the ${ctx.name} image and load it into a remote host's Docker, for a
|
|
58
|
+
# host that cannot pull it (the image is in no registry). norsk-ctl on that host
|
|
59
|
+
# then adds and launches it from the local copy: \`product add\` and the launch
|
|
60
|
+
# both treat a failed pull as "use the local image". For a customer's server,
|
|
61
|
+
# package-release.sh is the whole kit; this is the quick route for a dev box.
|
|
62
|
+
#
|
|
63
|
+
# bun run ship-image -- <ssh-host> # build, then ship
|
|
64
|
+
# SKIP_BUILD=1 bun run ship-image -- <ssh-host> # ship the existing image
|
|
65
|
+
set -euo pipefail
|
|
66
|
+
|
|
67
|
+
host="\${1:?usage: ship-image.sh <ssh-host>}"
|
|
68
|
+
export IMAGE_TAG="\${IMAGE_TAG:-${ctx.name}:dev}"
|
|
69
|
+
cd "$(dirname "\${BASH_SOURCE[0]}")/.."
|
|
70
|
+
|
|
71
|
+
[ -n "\${SKIP_BUILD:-}" ] || bun run build:image
|
|
72
|
+
|
|
73
|
+
# An image built for another architecture loads without complaint and then
|
|
74
|
+
# fails at launch, so refuse it here.
|
|
75
|
+
local_arch="$(docker image inspect -f '{{.Architecture}}' "\${IMAGE_TAG}")"
|
|
76
|
+
case "$(ssh "\${host}" uname -m)" in
|
|
77
|
+
x86_64) remote_arch=amd64 ;;
|
|
78
|
+
aarch64 | arm64) remote_arch=arm64 ;;
|
|
79
|
+
*) remote_arch=unknown ;;
|
|
80
|
+
esac
|
|
81
|
+
if [ "\${local_arch}" != "\${remote_arch}" ]; then
|
|
82
|
+
echo "\${IMAGE_TAG} is \${local_arch} but \${host} is \${remote_arch}: build on a \${remote_arch} machine" >&2
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
echo "shipping \${IMAGE_TAG} to \${host}"
|
|
87
|
+
docker save "\${IMAGE_TAG}" | gzip | ssh "\${host}" 'gunzip | docker load'
|
|
88
|
+
echo "loaded. On \${host}: norsk-ctl product add --image \${IMAGE_TAG} --license-file <licence>"
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const SERVER_KIT_GITIGNORE = `# A release (bun run package-release): the image tar and the scripts, built for
|
|
93
|
+
# a server, never source.
|
|
94
|
+
release/
|
|
95
|
+
`;
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from "./features/dashboard.ts";
|
|
39
39
|
import { CONFIG_SCREEN_URL, FRONTEND_SERVER_BLOCK, frontendFiles } from "./features/frontend.ts";
|
|
40
40
|
import { VIEWS_PROXY_EXPOSE, VIEWS_RUNTIME_SCREEN, viewsFiles } from "./features/views.ts";
|
|
41
|
+
import { packageReleaseSh, SERVER_KIT_GITIGNORE, shipImageSh } from "./server-kit.ts";
|
|
41
42
|
|
|
42
43
|
// The per-feature section of the generated CLAUDE.md: what each optional
|
|
43
44
|
// workspace is, and the trap that comes with it. Every line here is something
|
|
@@ -182,6 +183,15 @@ container port 4321), then \`bun run demo -- up --mode image --daemon reuse\`:
|
|
|
182
183
|
the driver deletes the instance, removes the template + product, re-adds the
|
|
183
184
|
fresh image and checks the pins before relaunching.
|
|
184
185
|
|
|
186
|
+
**A customer's server:** \`bun run package-release\` writes \`release/\`: the
|
|
187
|
+
image as a tar (tagged by commit, so a server tells releases apart), the
|
|
188
|
+
drift-gated \`deployment/install.sh\` and \`deployment/uninstall.sh\`, and
|
|
189
|
+
\`release.env\`. Copied to the server, \`install.sh --license <file>\` takes a
|
|
190
|
+
bare box to a running instance and upgrades one in place (norsk-ctl too,
|
|
191
|
+
media kept); every template parameter is taken from a same-named environment
|
|
192
|
+
variable. \`uninstall.sh\` restores the box and lists anything left. A dev box
|
|
193
|
+
with no registry access: \`bun run ship-image -- <ssh-host>\`.
|
|
194
|
+
|
|
185
195
|
## First run after generation
|
|
186
196
|
|
|
187
197
|
1. \`nix develop\` — the dev shell: pinned bun, biome and norsk-ctl.
|
|
@@ -359,6 +369,8 @@ function rootPackageJson(ctx: ShapeContext): string {
|
|
|
359
369
|
...(hasFrontend ? { "dev:frontend": "bun run --cwd frontend dev" } : {}),
|
|
360
370
|
start: "bun run --cwd backend start",
|
|
361
371
|
iterate: "bash deployment/iterate.sh",
|
|
372
|
+
"package-release": "bash deployment/package-release.sh",
|
|
373
|
+
"ship-image": "bash deployment/ship-image.sh",
|
|
362
374
|
demo: "ctl-demo",
|
|
363
375
|
},
|
|
364
376
|
devDependencies: {
|
|
@@ -1936,6 +1948,7 @@ shared/dist
|
|
|
1936
1948
|
.git
|
|
1937
1949
|
.github
|
|
1938
1950
|
*.md
|
|
1951
|
+
release
|
|
1939
1952
|
`;
|
|
1940
1953
|
|
|
1941
1954
|
function exampleInputJson(_ctx: ShapeContext): string {
|
|
@@ -2240,6 +2253,7 @@ function gitignoreTail(ctx: ShapeContext): string {
|
|
|
2240
2253
|
if (ctx.features.has("components")) blocks.push(COMPONENTS_GITIGNORE);
|
|
2241
2254
|
if (ctx.features.has("dashboard")) blocks.push(DASHBOARD_GITIGNORE);
|
|
2242
2255
|
blocks.push(DOCS_SITE_GITIGNORE);
|
|
2256
|
+
blocks.push(SERVER_KIT_GITIGNORE);
|
|
2243
2257
|
return blocks.join("\n");
|
|
2244
2258
|
}
|
|
2245
2259
|
|
|
@@ -2259,6 +2273,10 @@ export const turnkey: ShapeModule = {
|
|
|
2259
2273
|
{ path: ".dockerignore", content: DOCKERIGNORE },
|
|
2260
2274
|
{ path: "examples/default/input.json", content: exampleInputJson(ctx) },
|
|
2261
2275
|
{ path: "deployment/iterate.sh", content: iterateSh(ctx), executable: true },
|
|
2276
|
+
{ path: "deployment/package-release.sh", content: packageReleaseSh(ctx), executable: true },
|
|
2277
|
+
{ path: "deployment/ship-image.sh", content: shipImageSh(ctx), executable: true },
|
|
2278
|
+
{ path: "deployment/install.sh", content: ctx.canon.serverInstall, executable: true },
|
|
2279
|
+
{ path: "deployment/uninstall.sh", content: ctx.canon.serverUninstall, executable: true },
|
|
2262
2280
|
{ path: "scripts/demo", content: ctx.canon.demoShim, executable: true },
|
|
2263
2281
|
{ path: "README.md", content: readmeMd(ctx) },
|
|
2264
2282
|
...docsSiteFiles(ctx),
|