@esuyo/coding-container 1.0.2
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/Dockerfile +126 -0
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/config.yaml +27 -0
- package/package.json +39 -0
- package/src/commands/build.js +35 -0
- package/src/commands/create.js +38 -0
- package/src/commands/delete.js +24 -0
- package/src/commands/deploy.js +138 -0
- package/src/commands/gc.js +160 -0
- package/src/commands/push.js +36 -0
- package/src/commands/setup.js +190 -0
- package/src/commands/ssh.js +103 -0
- package/src/commands/system.js +220 -0
- package/src/commands/validate.js +86 -0
- package/src/config.js +586 -0
- package/src/index.js +281 -0
- package/src/utils/envsubst.js +39 -0
- package/src/utils/exec.js +71 -0
- package/src/utils/keys.js +30 -0
- package/src/utils/manifest.js +326 -0
- package/src/utils/project.js +106 -0
- package/src/utils/registry-api.js +308 -0
- package/src/utils/registry.js +204 -0
- package/src/utils/semver.js +42 -0
- package/src/utils/ssh-config.js +83 -0
- package/src/utils/system-config.js +162 -0
package/Dockerfile
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
#
|
|
3
|
+
# Coding container: Ubuntu + Node.js + opencode + pi + herdr + playwright + ssh
|
|
4
|
+
#
|
|
5
|
+
# Adjustable via build args (see config.yaml -> image.buildArgs):
|
|
6
|
+
# BASE_IMAGE Ubuntu base image
|
|
7
|
+
# NODE_VERSION Major Node.js version installed via NodeSource
|
|
8
|
+
# EXTRA_APT_PACKAGES Space separated list of extra apt packages
|
|
9
|
+
# EXTRA_NPM_PACKAGES Space separated list of extra global npm packages
|
|
10
|
+
# INSTALL_PLAYWRIGHT "true" (default) or "false"
|
|
11
|
+
|
|
12
|
+
ARG BASE_IMAGE=ubuntu:26.04
|
|
13
|
+
FROM ${BASE_IMAGE}
|
|
14
|
+
|
|
15
|
+
ARG NODE_VERSION=22
|
|
16
|
+
ARG EXTRA_APT_PACKAGES=""
|
|
17
|
+
ARG EXTRA_NPM_PACKAGES=""
|
|
18
|
+
ARG INSTALL_PLAYWRIGHT=true
|
|
19
|
+
|
|
20
|
+
ENV DEBIAN_FRONTEND=noninteractive \
|
|
21
|
+
PLAYWRIGHT_BROWSERS_PATH=/usr/lib/playwright-browsers
|
|
22
|
+
|
|
23
|
+
# --- Base tooling -----------------------------------------------------------
|
|
24
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
25
|
+
ca-certificates curl wget gnupg git \
|
|
26
|
+
openssh-server \
|
|
27
|
+
build-essential \
|
|
28
|
+
htop vim nano less tree unzip zip rsync \
|
|
29
|
+
jq fd-find ripgrep \
|
|
30
|
+
iputils-ping bind9-dnsutils \
|
|
31
|
+
${EXTRA_APT_PACKAGES} \
|
|
32
|
+
&& ( [ -x /usr/bin/fd-find ] && ln -sf /usr/bin/fd-find /usr/local/bin/fd \
|
|
33
|
+
|| ln -sf /usr/bin/fdfind /usr/local/bin/fd ) \
|
|
34
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
35
|
+
|
|
36
|
+
# --- Node.js (NodeSource) ---------------------------------------------------
|
|
37
|
+
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
|
|
38
|
+
&& apt-get install -y --no-install-recommends nodejs \
|
|
39
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
40
|
+
|
|
41
|
+
# --- Package managers -------------------------------------------------------
|
|
42
|
+
# corepack exposes pnpm/yarn (and npm) shims that honor each repo's
|
|
43
|
+
# packageManager pin (e.g. "pnpm@11.6.0"); a repo pin not matching the
|
|
44
|
+
# pre-fetched versions is still downloaded on first use. Pre-fetch the
|
|
45
|
+
# current pnpm/yarn (incl. pnpm's native binary, pulled on first run) at
|
|
46
|
+
# build time so first use inside a pod is offline-fast.
|
|
47
|
+
RUN corepack enable \
|
|
48
|
+
&& corepack prepare pnpm@latest --activate \
|
|
49
|
+
&& corepack prepare yarn@latest --activate \
|
|
50
|
+
&& pnpm --version >/dev/null \
|
|
51
|
+
&& yarn --version >/dev/null
|
|
52
|
+
|
|
53
|
+
# --- Native module build headers ---------------------------------------------
|
|
54
|
+
# Headers for node-gyp / prebuild-install so native npm packages (sharp,
|
|
55
|
+
# bcrypt, sqlite3, ...) compile at npm install time instead of failing.
|
|
56
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
57
|
+
pkg-config \
|
|
58
|
+
zlib1g-dev libssl-dev libpq-dev libsqlite3-dev libffi-dev \
|
|
59
|
+
libxml2-dev libxslt1-dev libpng-dev libbz2-dev liblzma-dev \
|
|
60
|
+
uuid-dev libkrb5-dev libcairo2-dev \
|
|
61
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
62
|
+
|
|
63
|
+
# --- Coding agents / CLIs ---------------------------------------------------
|
|
64
|
+
# opencode (installer adds PATH for interactive shells only; ENV + profile.d
|
|
65
|
+
# cover non-interactive shells and login shells)
|
|
66
|
+
RUN curl -fsSL https://opencode.ai/install | bash
|
|
67
|
+
ENV PATH="/root/.opencode/bin:${PATH}"
|
|
68
|
+
RUN printf 'export PATH="/root/.opencode/bin:$PATH"\n' > /etc/profile.d/opencode.sh
|
|
69
|
+
# pi (pi.dev)
|
|
70
|
+
RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
|
71
|
+
# herdr (https://herdr.dev) - terminal multiplexer/runtime for coding agents.
|
|
72
|
+
# Install to /usr/local/bin so it's on PATH for all shells incl. sshd sessions.
|
|
73
|
+
RUN curl -fsSL https://herdr.dev/install.sh | HERDR_INSTALL_DIR=/usr/local/bin sh \
|
|
74
|
+
&& herdr --version
|
|
75
|
+
# turbo (monorepo runner)
|
|
76
|
+
RUN npm install -g turbo
|
|
77
|
+
# extra global npm packages
|
|
78
|
+
RUN if [ -n "${EXTRA_NPM_PACKAGES}" ]; then npm install -g ${EXTRA_NPM_PACKAGES}; fi
|
|
79
|
+
|
|
80
|
+
# --- Playwright -------------------------------------------------------------
|
|
81
|
+
RUN if [ "${INSTALL_PLAYWRIGHT}" = "true" ]; then \
|
|
82
|
+
npm install -g playwright \
|
|
83
|
+
&& npx playwright install --with-deps chrome; \
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# --- Database clients -------------------------------------------------------
|
|
87
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
88
|
+
postgresql-client \
|
|
89
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
90
|
+
|
|
91
|
+
# --- SSH server -------------------------------------------------------------
|
|
92
|
+
RUN mkdir -p /root/.ssh && \
|
|
93
|
+
chmod 700 /root/.ssh && \
|
|
94
|
+
touch /root/.ssh/authorized_keys && \
|
|
95
|
+
chmod 600 /root/.ssh/authorized_keys && \
|
|
96
|
+
sed -ri 's/^#?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \
|
|
97
|
+
sed -ri 's/^#?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config && \
|
|
98
|
+
sed -ri 's/^#?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config && \
|
|
99
|
+
ssh-keygen -A
|
|
100
|
+
# /run may be a fresh tmpfs at start, so recreate the privsep dir at boot.
|
|
101
|
+
# sshd builds each session's environment from scratch (USER/HOME/PATH/SHELL +
|
|
102
|
+
# PAM), so the container's env (image ENV + pod spec) is not visible in SSH
|
|
103
|
+
# sessions; the entrypoint republishes it for login shells.
|
|
104
|
+
RUN cat > /entrypoint.sh <<'ENTRYPOINT' && chmod +x /entrypoint.sh
|
|
105
|
+
#!/bin/bash
|
|
106
|
+
mkdir -p /run/sshd
|
|
107
|
+
: > /etc/profile.d/99-container-env.sh
|
|
108
|
+
while IFS= read -r -d '' kv; do
|
|
109
|
+
name=${kv%%=*}
|
|
110
|
+
value=${kv#*=}
|
|
111
|
+
escaped=${value//"'"/"'\\''"}
|
|
112
|
+
printf "export %s='%s'\n" "$name" "$escaped"
|
|
113
|
+
done < /proc/self/environ >> /etc/profile.d/99-container-env.sh
|
|
114
|
+
exec /usr/sbin/sshd -D "$@"
|
|
115
|
+
ENTRYPOINT
|
|
116
|
+
|
|
117
|
+
# --- Workspace --------------------------------------------------------------
|
|
118
|
+
RUN mkdir -p /workspace
|
|
119
|
+
WORKDIR /workspace
|
|
120
|
+
# Land in /workspace on every interactive login
|
|
121
|
+
RUN printf '\ncd /workspace 2>/dev/null || cd ~\n' >> /root/.bashrc \
|
|
122
|
+
&& printf '\ncd /workspace 2>/dev/null || cd ~\n' >> /root/.profile
|
|
123
|
+
|
|
124
|
+
EXPOSE 22
|
|
125
|
+
|
|
126
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Esuyo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Esuyo coding-container
|
|
2
|
+
|
|
3
|
+
> Spin up a ready-to-code SSH container on your k3s cluster in one command.
|
|
4
|
+
|
|
5
|
+
`coding-container` gives every project its own consistent coding environment. One command deploys a container with Ubuntu 26.04, Node.js 22, [opencode](https://opencode.ai), [pi](https://pi.dev), [herdr](https://herdr.dev), turbo, Playwright, and common dev tools (plus pnpm/yarn via corepack and a Postgres client) to your k3s cluster. You SSH in and land in `/workspace` — a folder backed by your NAS, so your work survives restarts and stays separate per project.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- One command to deploy a ready-to-code container (`create`), SSH in (`ssh`), and tear down (`delete`)
|
|
10
|
+
- Persistent per-project workspace on your NAS — files survive restarts
|
|
11
|
+
- Generates a dedicated SSH key; VS Code Remote-SSH ready
|
|
12
|
+
- Optional browser access to dev servers and sidecars for databases and queues
|
|
13
|
+
- Shared team image with versioning, or a custom image per project
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- Node.js >= 18, Docker, `kubectl` pointed at your k3s cluster
|
|
18
|
+
- `docker login` to your container registry
|
|
19
|
+
- OpenSSH (`ssh`, `ssh-keygen`)
|
|
20
|
+
- Your NAS reachable from the cluster nodes
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install -g @esuyo/coding-container
|
|
26
|
+
coding-container --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or run from source:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install
|
|
33
|
+
npm link
|
|
34
|
+
coding-container --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`npm link` puts `coding-container` on your PATH. Prefer not to link globally? Run it with `node src/index.js ...` instead.
|
|
38
|
+
|
|
39
|
+
One-time setup (creates your global config and SSH key, never overwrites):
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
coding-container setup
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
Two small YAML files drive everything: a global config for your environment (`~/.config/coding-container/config.yaml`) and a per-project `config.yaml` for what differs per project.
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
project: my-project
|
|
51
|
+
k8s:
|
|
52
|
+
nodePort: 30022
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
See [Configuration](./docs/configuration.md) for layering, multiple projects, and NAS folders.
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
coding-container create # deploy this project
|
|
61
|
+
coding-container ssh # connect — you land in /workspace
|
|
62
|
+
coding-container delete # remove cluster resources (files stay safe on the NAS)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Working in another folder? `cd` there and run `create` — a minimal `config.yaml` is scaffolded automatically. Each project gets its own container and workspace.
|
|
66
|
+
|
|
67
|
+
For everything else — web URLs, databases, image versions, all commands and flags — see [CLI reference](./docs/cli-reference.md) and the guides below.
|
|
68
|
+
|
|
69
|
+
## Troubleshooting
|
|
70
|
+
|
|
71
|
+
- **Missing registry or NAS settings?** Run `coding-container setup`.
|
|
72
|
+
- **Push auth error?** Run `docker login <your-registry>` first.
|
|
73
|
+
- **Pod stuck?** Usually NFS or image pull — see [Troubleshooting](./docs/troubleshooting.md).
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT — see [LICENSE](./LICENSE).
|
|
78
|
+
|
|
79
|
+
## Developer Documentation
|
|
80
|
+
|
|
81
|
+
- [Configuration](./docs/configuration.md) — Config layering, multiple projects, NAS volumes
|
|
82
|
+
- [Shared global image](./docs/global-image.md) — Team image versions, publishing, rollback, cleanup
|
|
83
|
+
- [Web access](./docs/web-access.md) — Browser access to dev servers via Traefik
|
|
84
|
+
- [Sidecars](./docs/sidecars.md) — Databases, queues, and other pod companions
|
|
85
|
+
- [CLI reference](./docs/cli-reference.md) — All commands and flags
|
|
86
|
+
- [SSH and VS Code](./docs/ssh-and-vscode.md) — Key rotation and Remote-SSH setup
|
|
87
|
+
- [Troubleshooting](./docs/troubleshooting.md) — Common failures and fixes
|
|
88
|
+
- [Architecture](./docs/architecture.md) — Tech stack and system design
|
|
89
|
+
- [Project Structure](./docs/project-structure.md) — Folder layout
|
|
90
|
+
- [Development Guide](./docs/development.md) — Local setup for contributors
|
|
91
|
+
- [App config & setup files](./docs/config-files.md) — Persisting tool configs via /workspace
|
|
92
|
+
- [Plan: Additional NFS volumes](./docs/build/additional-volumes.md) — Design history for extra volumes
|
|
93
|
+
- [Plan: Public prebuilt images](./docs/build/public-images.md) — Design history for public images
|
|
94
|
+
|
|
95
|
+
---
|
package/config.yaml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Project-specific configuration for this repo's own container.
|
|
2
|
+
#
|
|
3
|
+
# Shared settings (registry, image build, k8s, nfs, ssh) come from the
|
|
4
|
+
# global config at ~/.config/coding-container/config.yaml; CLI flags override
|
|
5
|
+
# everything. Add keys here only for values that differ per project.
|
|
6
|
+
|
|
7
|
+
project: my-project
|
|
8
|
+
|
|
9
|
+
k8s:
|
|
10
|
+
nodePort: 30022
|
|
11
|
+
|
|
12
|
+
# Expose container ports to the browser through Traefik: one Ingress host per
|
|
13
|
+
# port, each routing to that port on this project's service. The hosts must
|
|
14
|
+
# resolve (DNS) to your Traefik LoadBalancer IP. An optional nodePort
|
|
15
|
+
# (30000-32767) additionally reaches the port on every cluster node at
|
|
16
|
+
# http://<node-ip>:<nodePort>/ (unique across ssh, web.ports, sidecars).
|
|
17
|
+
# web:
|
|
18
|
+
# ports:
|
|
19
|
+
# - { port: 3000, host: my-web.example.com, nodePort: 30080 }
|
|
20
|
+
# - { port: 8080, host: my-api.example.com, name: api }
|
|
21
|
+
|
|
22
|
+
# Extra NAS folders mounted into the container alongside /workspace. Each
|
|
23
|
+
# subPath is a subfolder of nfs.basePath (created on first mount if missing);
|
|
24
|
+
# readOnly: true makes the mount read-only for the container.
|
|
25
|
+
# nfs:
|
|
26
|
+
# volumes:
|
|
27
|
+
# - { subPath: shared-datasets, mountPath: /data, readOnly: true }
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@esuyo/coding-container",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "CLI to build, publish and deploy SSH-accessible coding containers on k3s",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"coding-container": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"dev": "node src/index.js",
|
|
13
|
+
"test": "node --test"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"docker",
|
|
20
|
+
"kubernetes",
|
|
21
|
+
"k3s",
|
|
22
|
+
"ssh",
|
|
23
|
+
"coding-container",
|
|
24
|
+
"devcontainer"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src/",
|
|
32
|
+
"Dockerfile",
|
|
33
|
+
"config.yaml"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^12.1.0",
|
|
37
|
+
"js-yaml": "^4.1.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { dockerExec, run } from "../utils/exec.js";
|
|
2
|
+
import { imageRef } from "../config.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The raw docker build of cfg's image (cfg.image.tag must be set). Used by
|
|
6
|
+
* custom-mode projects and by `system create` (the only writer of the
|
|
7
|
+
* global image).
|
|
8
|
+
*/
|
|
9
|
+
export async function buildImage(cfg) {
|
|
10
|
+
const ref = imageRef(cfg);
|
|
11
|
+
const args = ["build", "-t", ref, "-f", cfg.image.dockerfilePath];
|
|
12
|
+
for (const [key, value] of Object.entries(cfg.image.buildArgs || {})) {
|
|
13
|
+
args.push("--build-arg", `${key}=${value}`);
|
|
14
|
+
}
|
|
15
|
+
args.push(cfg.image.contextPath);
|
|
16
|
+
console.log(`Building image ${ref} ...`);
|
|
17
|
+
const { cmd, opts } = dockerExec();
|
|
18
|
+
await run(cmd, args, opts);
|
|
19
|
+
console.log(`Built ${ref}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function build(cfg) {
|
|
23
|
+
if (cfg.imageMode === "global") {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"this project uses the shared global image — building here would tag the global repo. " +
|
|
26
|
+
"Publish a new global version with: coding-container system create --tag vX.Y.Z"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (!cfg.image.tag) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"custom image mode requires a tag: set image.tag in the project config or pass --tag"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
await buildImage(cfg);
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { build } from "./build.js";
|
|
2
|
+
import { push } from "./push.js";
|
|
3
|
+
import { deploy } from "./deploy.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Global mode: nothing is built or pushed here — the shared image is
|
|
7
|
+
* managed by `system create`, and deploy floats on the cluster ConfigMap.
|
|
8
|
+
* Custom mode: build + push the project's own image (coding-<project>),
|
|
9
|
+
* then deploy.
|
|
10
|
+
*/
|
|
11
|
+
export async function create(cfg, { skipBuild = false, skipPush = false } = {}) {
|
|
12
|
+
if (cfg.imageMode === "global") {
|
|
13
|
+
if (skipBuild || skipPush) {
|
|
14
|
+
console.log(
|
|
15
|
+
"Global image mode: no build/push happens here — the shared image is managed by " +
|
|
16
|
+
"'coding-container system create'."
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
await deploy(cfg);
|
|
20
|
+
} else {
|
|
21
|
+
if (skipBuild) {
|
|
22
|
+
console.log("Skipping build");
|
|
23
|
+
} else {
|
|
24
|
+
await build(cfg);
|
|
25
|
+
}
|
|
26
|
+
if (skipPush) {
|
|
27
|
+
console.log("Skipping push");
|
|
28
|
+
} else {
|
|
29
|
+
await push(cfg);
|
|
30
|
+
}
|
|
31
|
+
await deploy(cfg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`
|
|
35
|
+
Done. Connect with:
|
|
36
|
+
coding-container ssh
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { run } from "../utils/exec.js";
|
|
2
|
+
import { removeHostEntry, sshAlias, sshConfigPath } from "../utils/ssh-config.js";
|
|
3
|
+
|
|
4
|
+
export async function deleteProject(cfg, { keepSecrets = false } = {}) {
|
|
5
|
+
const ns = cfg.k8s.namespace;
|
|
6
|
+
await run("kubectl", ["delete", "deployment", cfg.project, "-n", ns, "--ignore-not-found"]);
|
|
7
|
+
await run("kubectl", ["delete", "service", cfg.project, "-n", ns, "--ignore-not-found"]);
|
|
8
|
+
await run("kubectl", ["delete", "ingress", cfg.project, "-n", ns, "--ignore-not-found"]);
|
|
9
|
+
if (!keepSecrets) {
|
|
10
|
+
await run("kubectl", ["delete", "configmap", `${cfg.project}-ssh`, "-n", ns, "--ignore-not-found"]);
|
|
11
|
+
await run("kubectl", ["delete", "secret", `${cfg.project}-env`, "-n", ns, "--ignore-not-found"]);
|
|
12
|
+
}
|
|
13
|
+
console.log(`Deleted ${cfg.project} from namespace ${ns}`);
|
|
14
|
+
|
|
15
|
+
// Drop the SSH alias for the container.
|
|
16
|
+
try {
|
|
17
|
+
const alias = sshAlias(cfg.project);
|
|
18
|
+
if (removeHostEntry(alias)) {
|
|
19
|
+
console.log(`Removed SSH alias "${alias}" from ${sshConfigPath()}`);
|
|
20
|
+
}
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.warn(`Warning: could not update SSH config: ${err.message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { exec, run, runWithInput } from "../utils/exec.js";
|
|
3
|
+
import { ensureKeyPair, readPublicKey } from "../utils/keys.js";
|
|
4
|
+
import { ensurePullSecret } from "../utils/registry.js";
|
|
5
|
+
import { fullManifest, webPorts, sidecarNodePorts as sidecarNodePortsList } from "../utils/manifest.js";
|
|
6
|
+
import { imageRef } from "../config.js";
|
|
7
|
+
import { addHostEntry, sshAlias, sshConfigPath } from "../utils/ssh-config.js";
|
|
8
|
+
import { findNodePortOwner } from "../utils/project.js";
|
|
9
|
+
import { kubectlExec, resolveDeployTag, SYSTEM_CM_NAME } from "../utils/system-config.js";
|
|
10
|
+
|
|
11
|
+
export async function ensureNamespace(ns) {
|
|
12
|
+
const { cmd, args, opts } = kubectlExec();
|
|
13
|
+
try {
|
|
14
|
+
exec(cmd, [...args, "get", "namespace", ns], opts);
|
|
15
|
+
} catch {
|
|
16
|
+
console.log(`Creating namespace ${ns} ...`);
|
|
17
|
+
await run(cmd, [...args, "create", "namespace", ns], opts);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function deploy(cfg, { wait = true } = {}) {
|
|
22
|
+
// Tag resolution (the float): custom mode uses the project pin; global
|
|
23
|
+
// mode uses the project pin/--tag when set, else the ConfigMap's current.
|
|
24
|
+
const hadExplicitTag = Boolean(cfg.image.tag);
|
|
25
|
+
cfg.image.tag = resolveDeployTag(cfg);
|
|
26
|
+
if (cfg.imageMode === "global" && !hadExplicitTag) {
|
|
27
|
+
console.log(
|
|
28
|
+
`Global image mode: deploying version ${cfg.image.tag} from the ${SYSTEM_CM_NAME} ConfigMap ` +
|
|
29
|
+
`('system get' to inspect, 'system rollback' to repoint, 'system create --tag vX.Y.Z' to publish new).`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { public: publicPath } = ensureKeyPair(cfg.ssh.keyDirPath, cfg.ssh.keyName);
|
|
34
|
+
const publicKey = readPublicKey(publicPath);
|
|
35
|
+
|
|
36
|
+
const ns = cfg.k8s.namespace;
|
|
37
|
+
await ensureNamespace(ns);
|
|
38
|
+
|
|
39
|
+
// Fail fast on a taken NodePort: applying anyway leaves a partial state
|
|
40
|
+
// (deployment + ingress created, service invalid).
|
|
41
|
+
if (cfg.k8s.serviceType === "NodePort") {
|
|
42
|
+
const wanted = [
|
|
43
|
+
[cfg.k8s.nodePort, "k8s.nodePort (SSH)"],
|
|
44
|
+
...webPorts(cfg)
|
|
45
|
+
.filter((p) => p.nodePort !== undefined)
|
|
46
|
+
.map((p) => [p.nodePort, `web port "${p.name}"`]),
|
|
47
|
+
...sidecarNodePortsList(cfg).map((p) => [p.nodePort, `sidecar port "${p.name}"`]),
|
|
48
|
+
];
|
|
49
|
+
for (const [port, what] of wanted) {
|
|
50
|
+
const owner = findNodePortOwner(port);
|
|
51
|
+
// Our own service (redeploy with an unchanged port) is not a conflict.
|
|
52
|
+
if (owner && !(owner.namespace === ns && owner.name === cfg.project)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`nodePort ${port} (${what}) is already allocated to service "${owner.name}" ` +
|
|
55
|
+
`in namespace "${owner.namespace}"; pick another port (30000-32767) in config.yaml`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pullSecret = await ensurePullSecret(cfg);
|
|
62
|
+
|
|
63
|
+
const manifest = fullManifest(cfg, publicKey, pullSecret);
|
|
64
|
+
const { cmd: kubectl, args: kargs, opts: kopts } = kubectlExec();
|
|
65
|
+
console.log(`Deploying ${imageRef(cfg)} to namespace ${cfg.k8s.namespace} ...`);
|
|
66
|
+
await runWithInput(kubectl, [...kargs, "apply", "-f", "-"], manifest, kopts);
|
|
67
|
+
|
|
68
|
+
if (wait) {
|
|
69
|
+
console.log("Waiting for rollout to complete ...");
|
|
70
|
+
await run(kubectl, [
|
|
71
|
+
...kargs,
|
|
72
|
+
"rollout",
|
|
73
|
+
"status",
|
|
74
|
+
`deployment/${cfg.project}`,
|
|
75
|
+
"-n",
|
|
76
|
+
cfg.k8s.namespace,
|
|
77
|
+
"--timeout=300s",
|
|
78
|
+
], kopts);
|
|
79
|
+
}
|
|
80
|
+
console.log(`Deployed ${cfg.project} in ${cfg.k8s.namespace}`);
|
|
81
|
+
|
|
82
|
+
// Node IP for the NodePort hints below and the SSH alias.
|
|
83
|
+
let nodeIp;
|
|
84
|
+
try {
|
|
85
|
+
nodeIp = exec(kubectl, [
|
|
86
|
+
...kargs,
|
|
87
|
+
"get",
|
|
88
|
+
"nodes",
|
|
89
|
+
"-o",
|
|
90
|
+
"jsonpath={.items[0].status.addresses[?(@.type==\"InternalIP\")].address}",
|
|
91
|
+
], kopts);
|
|
92
|
+
} catch {
|
|
93
|
+
/* handled below */
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const web = webPorts(cfg);
|
|
97
|
+
if (web.length) {
|
|
98
|
+
console.log("Web (Traefik Ingress):");
|
|
99
|
+
for (const p of web) {
|
|
100
|
+
console.log(` http://${p.host}/ (container port ${p.port})`);
|
|
101
|
+
}
|
|
102
|
+
const webNodePorts = web.filter((p) => p.nodePort !== undefined);
|
|
103
|
+
if (webNodePorts.length) {
|
|
104
|
+
console.log("Web NodePorts (reachable on any cluster node):");
|
|
105
|
+
for (const p of webNodePorts) {
|
|
106
|
+
console.log(` http://${nodeIp ?? "<node-ip>"}:${p.nodePort}/ (container port ${p.port})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sidecarNodePorts = sidecarNodePortsList(cfg);
|
|
112
|
+
if (sidecarNodePorts.length) {
|
|
113
|
+
console.log("Sidecar NodePorts (reachable on any cluster node):");
|
|
114
|
+
for (const p of sidecarNodePorts) {
|
|
115
|
+
console.log(` ${p.name}: ${nodeIp ?? "<node-ip>"}:${p.nodePort} (container port ${p.port})`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Register an SSH alias for the container (VS Code Remote-SSH / ssh).
|
|
120
|
+
if (nodeIp) {
|
|
121
|
+
try {
|
|
122
|
+
const alias = sshAlias(cfg.project);
|
|
123
|
+
addHostEntry({
|
|
124
|
+
alias,
|
|
125
|
+
hostName: nodeIp,
|
|
126
|
+
port: cfg.k8s.nodePort,
|
|
127
|
+
user: cfg.ssh.user,
|
|
128
|
+
identityFile: path.join(cfg.ssh.keyDirPath, cfg.ssh.keyName),
|
|
129
|
+
knownHostsFile: path.join(cfg.ssh.keyDirPath, `${cfg.project}-known_hosts`),
|
|
130
|
+
});
|
|
131
|
+
console.log(`SSH alias "${alias}" written to ${sshConfigPath()} (connect: ssh ${alias})`);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.warn(`Warning: could not update SSH config: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
console.warn("Warning: could not resolve a node IP; skipped the SSH alias in ~/.ssh/config");
|
|
137
|
+
}
|
|
138
|
+
}
|