devs-common 4.0.4__tar.gz → 5.0.0__tar.gz
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.
- {devs_common-4.0.4 → devs_common-5.0.0}/PKG-INFO +1 -1
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/core/container.py +55 -1
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/Dockerfile +59 -2
- devs_common-5.0.0/devs_common/templates/README-vscode-settings.md +44 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/devcontainer.json +19 -50
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/extensions/devs-bridge-drop.vsix +0 -0
- devs_common-5.0.0/devs_common/templates/machine-settings.json +61 -0
- devs_common-5.0.0/devs_common/templates/machine-settings.tmux.json +6 -0
- devs_common-5.0.0/devs_common/templates/register-vscode-extensions.js +33 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/post-create-wrapper.sh +2 -0
- devs_common-5.0.0/devs_common/templates/scripts/post-start.sh +43 -0
- devs_common-5.0.0/devs_common/templates/scripts/setup-vscode-settings.sh +54 -0
- devs_common-5.0.0/devs_common/templates/scripts/start-tailscale.sh +158 -0
- devs_common-5.0.0/devs_common/templates/scripts/ts.sh +4 -0
- devs_common-5.0.0/devs_common/templates/sudo-scripts/setup-magicdns.sh +31 -0
- devs_common-5.0.0/devs_common/templates/sudo-scripts/start-tailscaled.sh +36 -0
- devs_common-5.0.0/devs_common/templates/sudo-scripts/ts-cli.sh +5 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/utils/devcontainer.py +3 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common.egg-info/PKG-INFO +1 -1
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common.egg-info/SOURCES.txt +11 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/pyproject.toml +1 -1
- {devs_common-4.0.4 → devs_common-5.0.0}/LICENSE +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/README.md +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/__init__.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/config.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/core/__init__.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/core/project.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/core/workspace.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/devs_config.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/exceptions.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/extensions/README.md +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/extensions/install.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/copy-ssh.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/install-bridge-drop.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/setup-workspace.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/start-tunnel.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/tunnel-status.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/sudo-scripts/setup-devs-env.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/vscode-settings-reference.md +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/utils/__init__.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/utils/config_hash.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/utils/console.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/utils/devcontainer_template.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/utils/docker_client.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/utils/file_utils.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/utils/git_utils.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common/utils/repo_cache.py +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common.egg-info/dependency_links.txt +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common.egg-info/requires.txt +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/devs_common.egg-info/top_level.txt +0 -0
- {devs_common-4.0.4 → devs_common-5.0.0}/setup.cfg +0 -0
|
@@ -332,6 +332,7 @@ class ContainerManager:
|
|
|
332
332
|
# Container exists but not running with matching config, just restart it
|
|
333
333
|
console.print(f" 🔄 Restarting stopped container...")
|
|
334
334
|
self.docker.start_container(existing_container['name'])
|
|
335
|
+
self._run_post_start(existing_container['name'])
|
|
335
336
|
console.print(f" ✅ Container restarted successfully")
|
|
336
337
|
return True
|
|
337
338
|
else:
|
|
@@ -422,6 +423,50 @@ class ContainerManager:
|
|
|
422
423
|
|
|
423
424
|
raise ContainerError(f"Failed to ensure container running for {dev_name}: {e}")
|
|
424
425
|
|
|
426
|
+
def _run_post_start(self, container_name: str) -> None:
|
|
427
|
+
"""Run the container's postStart hook after a raw Docker start.
|
|
428
|
+
|
|
429
|
+
``postStartCommand`` is a devcontainer-CLI concept: it fires on ``devcontainer
|
|
430
|
+
up`` and on a Dev Containers attach, but a plain ``docker start`` (which is how
|
|
431
|
+
we resume an already-built container) runs only PID 1 and skips it entirely.
|
|
432
|
+
Without this the hook's whole point is lost on the commonest restart path —
|
|
433
|
+
tailscaled and the tailnet node stay down, and VS Code settings aren't
|
|
434
|
+
re-deployed. The script is idempotent, so running it again is harmless. Never
|
|
435
|
+
raises: a container without the devs template simply has no such script.
|
|
436
|
+
"""
|
|
437
|
+
try:
|
|
438
|
+
self.docker.exec_command(container_name, "/usr/local/bin/post-start.sh")
|
|
439
|
+
except Exception:
|
|
440
|
+
pass # not a devs-template container / exec unavailable
|
|
441
|
+
|
|
442
|
+
def _deregister_tailnet_node(self, container_name: str, container_id: str) -> None:
|
|
443
|
+
"""Best-effort tailnet cleanup when a container is destroyed (not just stopped).
|
|
444
|
+
|
|
445
|
+
Ephemeral Tailscale nodes auto-reap when the container stops, but that lags a
|
|
446
|
+
few minutes — long enough that an immediate recreate gets a clash-suffixed name
|
|
447
|
+
(…-eamonn-1). A graceful ``tailscale logout`` while the container is STILL UP
|
|
448
|
+
deregisters the node now, freeing the name for reuse. Then prune the writeback
|
|
449
|
+
handshake file start-tailscale.sh left in the env mount (nothing else reaps it;
|
|
450
|
+
the ``devs vscode`` resolver only reads files for containers that still exist, so
|
|
451
|
+
a missed prune is self-correcting). Never raises — teardown must not be blocked
|
|
452
|
+
by tailnet cleanup, and non-tailnet containers simply no-op here.
|
|
453
|
+
"""
|
|
454
|
+
# 1. Graceful logout while the daemon is still alive (frees the tailnet name now).
|
|
455
|
+
try:
|
|
456
|
+
self.docker.exec_command(container_name, "sudo -n /usr/local/bin/ts-cli.sh logout")
|
|
457
|
+
except Exception:
|
|
458
|
+
pass # no tailscale in this container / daemon down / exec unavailable
|
|
459
|
+
|
|
460
|
+
# 2. Prune the handshake file (keyed by the container's short id == its hostname).
|
|
461
|
+
try:
|
|
462
|
+
nodes_dir = get_env_mount_path(self.project.info.name) / "ts-nodes"
|
|
463
|
+
if nodes_dir.is_dir():
|
|
464
|
+
for handshake in nodes_dir.glob("*.json"):
|
|
465
|
+
if container_id and container_id.startswith(handshake.stem):
|
|
466
|
+
handshake.unlink()
|
|
467
|
+
except Exception:
|
|
468
|
+
pass
|
|
469
|
+
|
|
425
470
|
def stop_container(self, dev_name: str, remove: bool = True) -> bool:
|
|
426
471
|
"""Stop a container by labels, optionally removing it.
|
|
427
472
|
|
|
@@ -442,6 +487,11 @@ class ContainerManager:
|
|
|
442
487
|
for container_info in existing_containers:
|
|
443
488
|
container_name = container_info['name']
|
|
444
489
|
|
|
490
|
+
if remove:
|
|
491
|
+
# Free the tailnet node + handshake BEFORE stopping — logout
|
|
492
|
+
# needs the container's tailscaled still running.
|
|
493
|
+
self._deregister_tailnet_node(container_name, container_info.get('id', ''))
|
|
494
|
+
|
|
445
495
|
try:
|
|
446
496
|
self.docker.stop_container(container_name)
|
|
447
497
|
except DockerError as stop_e:
|
|
@@ -600,7 +650,10 @@ class ContainerManager:
|
|
|
600
650
|
for container in containers:
|
|
601
651
|
try:
|
|
602
652
|
console.print(f" 🗑️ Removing aborted container: {container.name} ({container.status})")
|
|
603
|
-
|
|
653
|
+
|
|
654
|
+
# Best-effort tailnet cleanup (logout if still up + prune handshake file).
|
|
655
|
+
self._deregister_tailnet_node(container.name, container.container_id or '')
|
|
656
|
+
|
|
604
657
|
# Stop container first if it's running
|
|
605
658
|
if container.status.lower() in ['running', 'restarting']:
|
|
606
659
|
console.print(f" 🛑 Stopping running container: {container.name}")
|
|
@@ -664,6 +717,7 @@ class ContainerManager:
|
|
|
664
717
|
container_id = existing_containers[0].get('name', container_name)
|
|
665
718
|
console.print(f" ▶️ Starting stopped container: {container_id}")
|
|
666
719
|
self.docker.start_container(container_id)
|
|
720
|
+
self._run_post_start(container_id)
|
|
667
721
|
else:
|
|
668
722
|
# Ensure container is running (may create/restart as needed, but never auto-rebuild)
|
|
669
723
|
if not self.ensure_container_running(dev_name, workspace_dir, check_rebuild=False, debug=debug, live=live, extra_env=extra_env):
|
|
@@ -35,6 +35,7 @@ RUN apt update && apt install -y less \
|
|
|
35
35
|
vim \
|
|
36
36
|
nano \
|
|
37
37
|
htop \
|
|
38
|
+
tmux \
|
|
38
39
|
&& apt clean \
|
|
39
40
|
&& rm -rf /var/lib/apt/lists/*
|
|
40
41
|
|
|
@@ -87,11 +88,33 @@ RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhisto
|
|
|
87
88
|
ENV DEVCONTAINER=true
|
|
88
89
|
|
|
89
90
|
# Create workspace and config directories and set permissions
|
|
90
|
-
RUN mkdir -p /workspaces /home/node/claudeconfig /home/node/codexconfig
|
|
91
|
+
RUN mkdir -p /workspaces /home/node/claudeconfig /home/node/codexconfig \
|
|
92
|
+
/home/node/.vscode-server/extensions \
|
|
93
|
+
/home/node/.vscode-server/data/Machine && \
|
|
91
94
|
echo '[]' > /home/node/.vscode-server/extensions/extensions.json && \
|
|
92
95
|
chown -R node:node /workspaces /home/node /home/node/.vscode-server && \
|
|
93
96
|
chmod 755 /workspaces
|
|
94
97
|
|
|
98
|
+
# Pre-install workspace-kind VS Code extensions so they're available on first connect.
|
|
99
|
+
# devcontainer.json's extensions list is only auto-installed by the Dev Containers
|
|
100
|
+
# extension; over an SSH remote it's silently skipped.
|
|
101
|
+
RUN for EXT in vscode-tmux-auto-reattach devs-bridge-drop; do \
|
|
102
|
+
curl -fSL --retry 3 --compressed \
|
|
103
|
+
"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ideonate/vsextensions/${EXT}/latest/vspackage" \
|
|
104
|
+
-o /tmp/ext.vsix && \
|
|
105
|
+
unzip -q /tmp/ext.vsix "extension/package.json" -d /tmp/ext-meta && \
|
|
106
|
+
VERSION=$(node -e "console.log(require('/tmp/ext-meta/extension/package.json').version)") && \
|
|
107
|
+
EXT_DIR="/home/node/.vscode-server/extensions/ideonate.${EXT}-${VERSION}" && \
|
|
108
|
+
mkdir -p "${EXT_DIR}" && \
|
|
109
|
+
unzip -q /tmp/ext.vsix "extension/*" -d /tmp/ext && \
|
|
110
|
+
cp -r /tmp/ext/extension/. "${EXT_DIR}/" && \
|
|
111
|
+
rm -rf /tmp/ext.vsix /tmp/ext /tmp/ext-meta; \
|
|
112
|
+
done && \
|
|
113
|
+
chown -R node:node /home/node/.vscode-server/extensions/
|
|
114
|
+
COPY register-vscode-extensions.js /tmp/
|
|
115
|
+
RUN node /tmp/register-vscode-extensions.js && rm /tmp/register-vscode-extensions.js && \
|
|
116
|
+
chown node:node /home/node/.vscode-server/extensions/extensions.json
|
|
117
|
+
|
|
95
118
|
WORKDIR /workspaces
|
|
96
119
|
|
|
97
120
|
# Install git-delta for better git diffs
|
|
@@ -143,6 +166,18 @@ RUN ARCH=$(dpkg --print-architecture) && \
|
|
|
143
166
|
rm /tmp/vscode-cli.tar.gz && \
|
|
144
167
|
chmod +x /usr/local/bin/code
|
|
145
168
|
|
|
169
|
+
# Install Tailscale (static binaries) so each devcontainer can join the tailnet
|
|
170
|
+
# as its own node in userspace-networking mode — see scripts/start-tailscale.sh.
|
|
171
|
+
# Pinned for reproducible builds; bump as needed.
|
|
172
|
+
ARG TAILSCALE_VERSION=1.98.4
|
|
173
|
+
RUN ARCH=$(dpkg --print-architecture) && \
|
|
174
|
+
curl -fSL --retry 5 --retry-delay 5 --retry-all-errors --connect-timeout 30 \
|
|
175
|
+
"https://pkgs.tailscale.com/stable/tailscale_${TAILSCALE_VERSION}_${ARCH}.tgz" -o /tmp/tailscale.tgz && \
|
|
176
|
+
tar -xzf /tmp/tailscale.tgz -C /tmp && \
|
|
177
|
+
mv "/tmp/tailscale_${TAILSCALE_VERSION}_${ARCH}/tailscale" "/tmp/tailscale_${TAILSCALE_VERSION}_${ARCH}/tailscaled" /usr/local/bin/ && \
|
|
178
|
+
rm -rf /tmp/tailscale.tgz "/tmp/tailscale_${TAILSCALE_VERSION}_${ARCH}" && \
|
|
179
|
+
chmod +x /usr/local/bin/tailscale /usr/local/bin/tailscaled
|
|
180
|
+
|
|
146
181
|
# Set up environment variables and aliases for Claude and Codex
|
|
147
182
|
RUN for shell_rc in /home/node/.zshrc /home/node/.bashrc; do \
|
|
148
183
|
echo "export CLAUDE_CONFIG_DIR=\"/home/node/claudeconfig\"" >> "$shell_rc" && \
|
|
@@ -159,11 +194,33 @@ RUN for shell_rc in /home/node/.zshrc /home/node/.bashrc; do \
|
|
|
159
194
|
echo "export TEST_RESULTS_FOLDER=\"/home/node/bridge/test-results\"" >> "$shell_rc"; \
|
|
160
195
|
done
|
|
161
196
|
|
|
197
|
+
# tmux defaults: mouse scrolling, deep scrollback, and live tab titles. Pairs with the
|
|
198
|
+
# ideonate.vscode-tmux-auto-reattach extension (liveTitles mode). Present regardless of
|
|
199
|
+
# DEVS_TMUX — this only shapes tmux when you actually run it.
|
|
200
|
+
RUN printf '%s\n' \
|
|
201
|
+
'set -g mouse on' \
|
|
202
|
+
'set -g history-limit 50000' \
|
|
203
|
+
'bind m set -g mouse \; display "mouse #{?mouse,on,off}"' \
|
|
204
|
+
'set -g set-titles on' \
|
|
205
|
+
"set -g set-titles-string '#{session_name}: #{pane_current_command}'" \
|
|
206
|
+
> /home/node/.tmux.conf && \
|
|
207
|
+
chown node:node /home/node/.tmux.conf
|
|
208
|
+
|
|
209
|
+
# VS Code settings live here rather than in devcontainer.json's
|
|
210
|
+
# customizations.vscode.settings, because that block is only applied by the Dev
|
|
211
|
+
# Containers extension — a Remote-SSH connection (e.g. over Tailscale SSH) never reads
|
|
212
|
+
# devcontainer.json. Baking them as machine settings covers both paths. Both files also
|
|
213
|
+
# land in /etc/devcontainer-config/ so scripts/setup-vscode-settings.sh can re-deploy
|
|
214
|
+
# them at container start with the optional tmux overlay (DEVS_TMUX=1) applied.
|
|
215
|
+
# See templates/README-vscode-settings.md.
|
|
216
|
+
COPY --chown=node:node machine-settings.json /home/node/.vscode-server/data/Machine/settings.json
|
|
217
|
+
COPY machine-settings.json machine-settings.tmux.json /etc/devcontainer-config/
|
|
162
218
|
|
|
163
219
|
# Copy and set up scripts
|
|
164
220
|
COPY scripts/ /usr/local/bin/
|
|
165
221
|
COPY sudo-scripts/ /usr/local/bin/
|
|
166
|
-
RUN chmod +x /usr/local/bin/*.sh /usr/local/bin/*.py
|
|
222
|
+
RUN chmod +x /usr/local/bin/*.sh /usr/local/bin/*.py && \
|
|
223
|
+
ln -sf /usr/local/bin/ts.sh /usr/local/bin/ts
|
|
167
224
|
COPY sudo-scripts/ /tmp/sudo-scripts-list/
|
|
168
225
|
RUN echo "DEBUG: Files that need sudo:" && ls -la /tmp/sudo-scripts-list/ && \
|
|
169
226
|
{ echo "# Allow node user to run sudo scripts without password"; \
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# VS Code settings in this template
|
|
2
|
+
|
|
3
|
+
`machine-settings.json` is baked into the image at
|
|
4
|
+
`~/.vscode-server/data/Machine/settings.json` (see the Dockerfile), **not** put in
|
|
5
|
+
`devcontainer.json` under `customizations.vscode.settings`.
|
|
6
|
+
|
|
7
|
+
Why: that `customizations` block is applied by the **Dev Containers extension only**. A
|
|
8
|
+
Remote-SSH connection — which is how you reach a container over Tailscale SSH — never reads
|
|
9
|
+
`devcontainer.json`, so it got neither settings nor extensions. Machine settings cover both
|
|
10
|
+
paths. For the same reason the `ideonate.*` extensions are unzipped into the image and
|
|
11
|
+
registered via `register-vscode-extensions.js`.
|
|
12
|
+
|
|
13
|
+
**Don't add a `settings` block back to `devcontainer.json`**: the Dev Containers extension
|
|
14
|
+
writes it over the same machine-settings file, and the two would silently drift.
|
|
15
|
+
|
|
16
|
+
## tmux (opt-in, off by default)
|
|
17
|
+
|
|
18
|
+
`machine-settings.json` *defines* a `tmux` terminal profile and the
|
|
19
|
+
`ideonate.vscode-tmux-auto-reattach` settings, but leaves the default profile as `zsh` and
|
|
20
|
+
`tmuxAutoReattach.runOnStartup` as `false`. Nothing changes unless you ask for it.
|
|
21
|
+
|
|
22
|
+
To turn it on, set `DEVS_TMUX=1` — via `DEVS.yml` `env_vars:`, `devs start … --env
|
|
23
|
+
DEVS_TMUX=1`, or a mounted `~/.devs/envs/<project>/.env`. On each container start,
|
|
24
|
+
`scripts/setup-vscode-settings.sh` merges `machine-settings.tmux.json` over the base file,
|
|
25
|
+
which flips the default profile to tmux and enables auto-reattach. Unset it and the next
|
|
26
|
+
start reverts to the base file — no rebuild needed either way.
|
|
27
|
+
|
|
28
|
+
You can still open a tmux terminal manually from the profile dropdown with `DEVS_TMUX`
|
|
29
|
+
unset; the profile is always defined.
|
|
30
|
+
|
|
31
|
+
### Notes on the tmux settings
|
|
32
|
+
|
|
33
|
+
- `~/.tmux.conf` (written in the Dockerfile) sets `mouse on`, 50k scrollback, and
|
|
34
|
+
`set-titles` so tab titles show the running command. Existing sessions keep their old
|
|
35
|
+
config — `tmux source-file ~/.tmux.conf` then start a new session, or `tmux kill-server`.
|
|
36
|
+
- With `mouse on`, click-drag and double-click go to tmux rather than VS Code.
|
|
37
|
+
`terminal.integrated.macOptionClickForcesSelection` (on in the base file) lets
|
|
38
|
+
Option-click/drag bypass tmux for native selection. `prefix + m` toggles mouse mode.
|
|
39
|
+
- `tmuxAutoReattach.shellPath=/bin/sh` runs the reattach as the terminal's own process.
|
|
40
|
+
Without it the terminal starts the default profile (`tmux new-session`) first and the
|
|
41
|
+
attach nests inside that tmux.
|
|
42
|
+
- `terminal.integrated.enablePersistentSessions: false` (tmux overlay) stops VS Code
|
|
43
|
+
reviving old terminal tabs on reload — each would launch the default profile before the
|
|
44
|
+
extension can reattach, again causing nested tmux.
|
|
@@ -22,55 +22,14 @@
|
|
|
22
22
|
"ms-python.black-formatter",
|
|
23
23
|
"ms-python.isort",
|
|
24
24
|
"Anthropic.claude-code",
|
|
25
|
-
"ideonate.devs-bridge-drop"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"editor.detectIndentation": false,
|
|
34
|
-
"editor.insertSpaces": true,
|
|
35
|
-
"editor.tabSize": 2,
|
|
36
|
-
"[python]": {
|
|
37
|
-
"editor.formatOnSave": true,
|
|
38
|
-
"editor.formatOnType": true,
|
|
39
|
-
"editor.tabSize": 4,
|
|
40
|
-
"editor.codeActionsOnSave": {
|
|
41
|
-
"source.organizeImports": "never"
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"python.defaultInterpreterPath": "/home/node/.devs-venv/workspace-venv/bin/python",
|
|
45
|
-
"python.terminal.activateEnvironment": true,
|
|
46
|
-
"python.interpreter.infoVisibility": "never",
|
|
47
|
-
"python.venvFolders": [".devs-venv"],
|
|
48
|
-
"python.languageServer": "Pylance",
|
|
49
|
-
"python.analysis.typeCheckingMode": "off",
|
|
50
|
-
"python.analysis.autoImportCompletions": true,
|
|
51
|
-
"python.analysis.diagnosticSeverityOverrides": {
|
|
52
|
-
"reportUnusedImport": "information",
|
|
53
|
-
"reportUnusedVariable": "information"
|
|
54
|
-
},
|
|
55
|
-
"pylint.enabled": false,
|
|
56
|
-
"gitlens.mode": "zen",
|
|
57
|
-
"python.analysis.diagnosticMode": "openFilesOnly",
|
|
58
|
-
"settingsSync.ignoredExtensions": [],
|
|
59
|
-
"settingsSync.keybindingsPerPlatform": false,
|
|
60
|
-
"extensions.autoCheckUpdates": false,
|
|
61
|
-
"extensions.autoUpdate": false,
|
|
62
|
-
"extensions.ignoreRecommendations": true,
|
|
63
|
-
"terminal.integrated.defaultProfile.linux": "zsh",
|
|
64
|
-
"terminal.integrated.profiles.linux": {
|
|
65
|
-
"bash": {
|
|
66
|
-
"path": "bash",
|
|
67
|
-
"icon": "terminal-bash"
|
|
68
|
-
},
|
|
69
|
-
"zsh": {
|
|
70
|
-
"path": "zsh"
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
25
|
+
"ideonate.devs-bridge-drop",
|
|
26
|
+
"ideonate.vscode-tmux-auto-reattach"
|
|
27
|
+
]
|
|
28
|
+
// VS Code settings live in .devcontainer/machine-settings.json — baked into
|
|
29
|
+
// the image via Dockerfile so they apply for both Dev Containers and SSH
|
|
30
|
+
// remote (Tailscale SSH never reads devcontainer.json). Don't add a
|
|
31
|
+
// "settings" block back here: the Dev Containers extension writes it over
|
|
32
|
+
// the same machine-settings file, and the two would drift.
|
|
74
33
|
}
|
|
75
34
|
},
|
|
76
35
|
"remoteUser": "node",
|
|
@@ -84,7 +43,11 @@
|
|
|
84
43
|
// "source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-extensions,target=/home/node/.vscode-server/extensions,type=volume",
|
|
85
44
|
// .vscode-server includes extension state which we don't want to keep
|
|
86
45
|
// "source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-data,target=/home/node/.vscode-server,type=volume",
|
|
87
|
-
"source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-downloads,target=/home/node/.vscode-extensions-cache-volume,type=volume"
|
|
46
|
+
"source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-downloads,target=/home/node/.vscode-extensions-cache-volume,type=volume",
|
|
47
|
+
// Persist the tailnet node identity so the container keeps one stable 100.x
|
|
48
|
+
// and hostname across rebuilds — otherwise every rebuild mints a new node and
|
|
49
|
+
// your saved VS Code SSH host stops resolving.
|
|
50
|
+
"source=${localEnv:WORKSPACE_FOLDER_NAME}-tailscale-state,target=/var/lib/tailscale,type=volume"
|
|
88
51
|
],
|
|
89
52
|
"remoteEnv": {
|
|
90
53
|
"NODE_OPTIONS": "--max-old-space-size=4096",
|
|
@@ -93,13 +56,19 @@
|
|
|
93
56
|
"LC_ALL": "en_US.UTF-8",
|
|
94
57
|
"GIT_REMOTE_URL": "${localEnv:GIT_REMOTE_URL}",
|
|
95
58
|
"WORKSPACE_FOLDER_NAME": "${localEnv:WORKSPACE_FOLDER_NAME}",
|
|
59
|
+
"DEVCONTAINER_NAME": "${localEnv:DEVCONTAINER_NAME}",
|
|
60
|
+
"DEVS_PROJECT_NAME": "${localEnv:DEVS_PROJECT_NAME}",
|
|
96
61
|
"DEVS_DEBUG": "${localEnv:DEVS_DEBUG}",
|
|
97
62
|
"DEVS_LIVE_MODE": "${localEnv:DEVS_LIVE_MODE}",
|
|
63
|
+
// Opt-in: makes tmux the default terminal profile and enables auto-reattach.
|
|
64
|
+
// Off unless set to 1/true/yes — see scripts/setup-vscode-settings.sh.
|
|
65
|
+
"DEVS_TMUX": "${localEnv:DEVS_TMUX}",
|
|
98
66
|
"DEVS_ENV_MOUNT_PATH": "${localEnv:DEVS_ENV_MOUNT_PATH}",
|
|
99
67
|
"DEVS_BRIDGE_MOUNT_PATH": "${localEnv:DEVS_BRIDGE_MOUNT_PATH}"
|
|
100
68
|
},
|
|
101
69
|
"workspaceFolder": "/workspaces/${localEnv:WORKSPACE_FOLDER_NAME}",
|
|
102
70
|
"postCreateCommand": "/usr/local/bin/post-create-wrapper.sh",
|
|
71
|
+
"postStartCommand": "/usr/local/bin/post-start.sh",
|
|
103
72
|
"forwardPorts": [],
|
|
104
73
|
"portsAttributes": {
|
|
105
74
|
"3306": {
|
{devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/extensions/devs-bridge-drop.vsix
RENAMED
|
Binary file
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"editor.formatOnSave": false,
|
|
3
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
4
|
+
"editor.codeActionsOnSave": {
|
|
5
|
+
"source.fixAll.eslint": "explicit"
|
|
6
|
+
},
|
|
7
|
+
"editor.detectIndentation": false,
|
|
8
|
+
"editor.insertSpaces": true,
|
|
9
|
+
"editor.tabSize": 2,
|
|
10
|
+
"[python]": {
|
|
11
|
+
"editor.formatOnSave": true,
|
|
12
|
+
"editor.formatOnType": true,
|
|
13
|
+
"editor.tabSize": 4,
|
|
14
|
+
"editor.codeActionsOnSave": {
|
|
15
|
+
"source.organizeImports": "never"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"python.defaultInterpreterPath": "/home/node/.devs-venv/workspace-venv/bin/python",
|
|
19
|
+
"python.terminal.activateEnvironment": true,
|
|
20
|
+
"python.interpreter.infoVisibility": "never",
|
|
21
|
+
"python.venvFolders": [".devs-venv"],
|
|
22
|
+
"python.languageServer": "Pylance",
|
|
23
|
+
"python.analysis.typeCheckingMode": "off",
|
|
24
|
+
"python.analysis.autoImportCompletions": true,
|
|
25
|
+
"python.analysis.diagnosticSeverityOverrides": {
|
|
26
|
+
"reportUnusedImport": "information",
|
|
27
|
+
"reportUnusedVariable": "information"
|
|
28
|
+
},
|
|
29
|
+
"pylint.enabled": false,
|
|
30
|
+
"gitlens.mode": "zen",
|
|
31
|
+
"python.analysis.diagnosticMode": "openFilesOnly",
|
|
32
|
+
"settingsSync.ignoredExtensions": [],
|
|
33
|
+
"settingsSync.keybindingsPerPlatform": false,
|
|
34
|
+
"extensions.autoCheckUpdates": false,
|
|
35
|
+
"extensions.autoUpdate": false,
|
|
36
|
+
"extensions.ignoreRecommendations": true,
|
|
37
|
+
"terminal.integrated.defaultProfile.linux": "zsh",
|
|
38
|
+
"terminal.integrated.profiles.linux": {
|
|
39
|
+
"bash": {
|
|
40
|
+
"path": "bash",
|
|
41
|
+
"icon": "terminal-bash"
|
|
42
|
+
},
|
|
43
|
+
"zsh": {
|
|
44
|
+
"path": "zsh"
|
|
45
|
+
},
|
|
46
|
+
"tmux": {
|
|
47
|
+
"path": "tmux",
|
|
48
|
+
"args": ["new-session", "-A", "-s", "main"],
|
|
49
|
+
"icon": "terminal-tmux"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"terminal.integrated.macOptionClickForcesSelection": true,
|
|
53
|
+
"tmuxAutoReattach.runOnStartup": false,
|
|
54
|
+
"tmuxAutoReattach.skipIfTerminalsOpen": true,
|
|
55
|
+
"tmuxAutoReattach.listCommand": "tmux list-sessions -F \"#{session_name}\"",
|
|
56
|
+
"tmuxAutoReattach.attachCommand": "tmux attach -t {session}",
|
|
57
|
+
"tmuxAutoReattach.shellPath": "/bin/sh",
|
|
58
|
+
"tmuxAutoReattach.shellArgs": ["-c"],
|
|
59
|
+
"tmuxAutoReattach.liveTitles": true,
|
|
60
|
+
"tmuxAutoReattach.titleCommand": ""
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Writes extensions.json so VS Code SSH remote discovers pre-installed extensions.
|
|
2
|
+
// VS Code trusts this registry rather than scanning the directory.
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const extDir = "/home/node/.vscode-server/extensions";
|
|
7
|
+
|
|
8
|
+
const entries = fs
|
|
9
|
+
.readdirSync(extDir)
|
|
10
|
+
.filter(
|
|
11
|
+
(d) =>
|
|
12
|
+
!d.endsWith(".json") && fs.statSync(path.join(extDir, d)).isDirectory(),
|
|
13
|
+
)
|
|
14
|
+
.flatMap((dir) => {
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(
|
|
17
|
+
fs.readFileSync(path.join(extDir, dir, "package.json"), "utf8"),
|
|
18
|
+
);
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
identifier: { id: `${pkg.publisher}.${pkg.name}` },
|
|
22
|
+
version: pkg.version,
|
|
23
|
+
location: { $mid: 1, path: path.join(extDir, dir), scheme: "file" },
|
|
24
|
+
relativeLocation: dir,
|
|
25
|
+
metadata: { installedTimestamp: 1700000000000, source: "vsix" },
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(path.join(extDir, "extensions.json"), JSON.stringify(entries));
|
{devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/post-create-wrapper.sh
RENAMED
|
@@ -37,6 +37,8 @@ run_step "setup-devs-env.sh" "/usr/local/bin/setup-devs-env.sh"
|
|
|
37
37
|
#run_step "init-firewall.sh" "sudo /usr/local/bin/init-firewall.sh"
|
|
38
38
|
run_step "setup-workspace.sh" "/usr/local/bin/setup-workspace.sh"
|
|
39
39
|
run_step "start-services.sh" "sudo /usr/local/bin/start-services.sh"
|
|
40
|
+
# Optional: join the tailnet + serve a port. Non-fatal — no auth key => no-op.
|
|
41
|
+
run_step "start-tailscale.sh" "/usr/local/bin/start-tailscale.sh" || true
|
|
40
42
|
|
|
41
43
|
# GitHub auth setup is optional, so handle it separately
|
|
42
44
|
echo "📋 Running gh auth setup-git..."
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# postStartCommand orchestrator (runs as the node user on every container start).
|
|
3
|
+
# Unprivileged: env/gating logic, then hands privileged work to root sudo-scripts.
|
|
4
|
+
#
|
|
5
|
+
# Why this runs on EVERY start (not just postCreate): a plain container restart
|
|
6
|
+
# (host reboot, `docker start`) re-runs only PID 1, not the devcontainer
|
|
7
|
+
# create-lifecycle. So tailscaled, the tailnet node and Tailscale SSH are all gone
|
|
8
|
+
# after a restart — and a VS Code Remote-SSH connection to that node then has
|
|
9
|
+
# nothing to reconnect to. Re-running the bring-up here restores it automatically.
|
|
10
|
+
# Every step is idempotent, so running it again right after postCreate (on first
|
|
11
|
+
# create) is a harmless no-op.
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
|
|
14
|
+
# Same env source start-tailscale.sh reads TS_ENABLE/TS_AUTHKEY from.
|
|
15
|
+
if [ -f /home/node/.devs-env/.env ]; then
|
|
16
|
+
set -a; . /home/node/.devs-env/.env; set +a
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# VS Code machine settings (+ optional tmux overlay, off unless DEVS_TMUX=1). Not
|
|
20
|
+
# Tailscale-gated — it configures the editor regardless of how you connect. Must come
|
|
21
|
+
# before the TS_ENABLE gate below, which exits when Tailscale is off.
|
|
22
|
+
/usr/local/bin/setup-vscode-settings.sh || true
|
|
23
|
+
|
|
24
|
+
# Gate on the same master switch as the rest of the Tailscale wiring.
|
|
25
|
+
case "${TS_ENABLE:-}" in
|
|
26
|
+
1|true|yes) ;;
|
|
27
|
+
*)
|
|
28
|
+
echo "ℹ️ Tailscale not enabled (TS_ENABLE=${TS_ENABLE:-unset}) — skipping tailnet setup."
|
|
29
|
+
exit 0 ;;
|
|
30
|
+
esac
|
|
31
|
+
|
|
32
|
+
# Re-join the tailnet (idempotent): ensures tailscaled is running — self-healing a
|
|
33
|
+
# stale socket left by the previous boot, see sudo-scripts/start-tailscaled.sh —
|
|
34
|
+
# then re-runs `tailscale up` (+ serve/funnel). The hostname derives from the same
|
|
35
|
+
# remoteEnv (DEVS_PROJECT_NAME/DEVCONTAINER_NAME) the create path uses, and the
|
|
36
|
+
# persisted /var/lib/tailscale state reclaims the same identity, so the node's
|
|
37
|
+
# name stays stable across restarts and rebuilds.
|
|
38
|
+
/usr/local/bin/start-tailscale.sh || true
|
|
39
|
+
|
|
40
|
+
# Prepend the MagicDNS resolver so tailnet split-DNS names resolve from inside the
|
|
41
|
+
# container. Must run after the bring-up above (it no-ops unless tailscaled is up),
|
|
42
|
+
# and on every start because Docker regenerates /etc/resolv.conf each time.
|
|
43
|
+
sudo -n /usr/local/bin/setup-magicdns.sh || true
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Deploy VS Code machine settings, applying the optional tmux overlay.
|
|
3
|
+
#
|
|
4
|
+
# Settings live in ~/.vscode-server/data/Machine/settings.json rather than in
|
|
5
|
+
# devcontainer.json's customizations.vscode.settings, because that block is applied by
|
|
6
|
+
# the Dev Containers extension only — a Remote-SSH connection (e.g. over Tailscale SSH)
|
|
7
|
+
# never reads devcontainer.json. See templates/README-vscode-settings.md.
|
|
8
|
+
#
|
|
9
|
+
# The Dockerfile already bakes the base file, so a container is correctly configured even
|
|
10
|
+
# if this never runs. This script exists for the DEVS_TMUX toggle: it re-deploys the base
|
|
11
|
+
# and merges machine-settings.tmux.json over it when tmux is requested, so switching is a
|
|
12
|
+
# container restart rather than an image rebuild.
|
|
13
|
+
#
|
|
14
|
+
# DEVS_TMUX "1"/"true"/"yes" => tmux becomes the default terminal profile and
|
|
15
|
+
# ideonate.vscode-tmux-auto-reattach reattaches sessions on window load.
|
|
16
|
+
# OFF by default: the tmux profile is defined either way, so you can pick
|
|
17
|
+
# it from the terminal dropdown without opting in globally.
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
BASE=/etc/devcontainer-config/machine-settings.json
|
|
21
|
+
OVERLAY=/etc/devcontainer-config/machine-settings.tmux.json
|
|
22
|
+
DEST=/home/node/.vscode-server/data/Machine/settings.json
|
|
23
|
+
|
|
24
|
+
[ -f "$BASE" ] || exit 0
|
|
25
|
+
|
|
26
|
+
# Same env source the other devs container scripts read their switches from.
|
|
27
|
+
if [ -f /home/node/.devs-env/.env ]; then
|
|
28
|
+
set -a; . /home/node/.devs-env/.env; set +a
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
mkdir -p "$(dirname "$DEST")"
|
|
32
|
+
|
|
33
|
+
case "${DEVS_TMUX:-}" in
|
|
34
|
+
1|true|yes)
|
|
35
|
+
if [ -f "$OVERLAY" ] && node -e '
|
|
36
|
+
const fs = require("fs");
|
|
37
|
+
const [base, overlay, dest] = process.argv.slice(1);
|
|
38
|
+
const merged = {
|
|
39
|
+
...JSON.parse(fs.readFileSync(base, "utf8")),
|
|
40
|
+
...JSON.parse(fs.readFileSync(overlay, "utf8")),
|
|
41
|
+
};
|
|
42
|
+
fs.writeFileSync(dest, JSON.stringify(merged, null, 2) + "\n");
|
|
43
|
+
' "$BASE" "$OVERLAY" "$DEST"; then
|
|
44
|
+
echo "🖥️ VS Code settings deployed (tmux enabled — DEVS_TMUX=${DEVS_TMUX})."
|
|
45
|
+
else
|
|
46
|
+
cp "$BASE" "$DEST"
|
|
47
|
+
echo "⚠️ tmux overlay merge failed — deployed base VS Code settings instead."
|
|
48
|
+
fi
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
cp "$BASE" "$DEST"
|
|
52
|
+
echo "🖥️ VS Code settings deployed (tmux off; set DEVS_TMUX=1 to enable)."
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Join this devcontainer to the user's tailnet as its own node, and optionally
|
|
3
|
+
# publish a port over HTTPS (serve/funnel) and/or accept Tailscale SSH. Invoked (as
|
|
4
|
+
# the node user) from post-create-wrapper.sh and post-start.sh — the latter so the
|
|
5
|
+
# node comes back after a restart. Safe no-op unless TS_ENABLE=1.
|
|
6
|
+
#
|
|
7
|
+
# This orchestrator runs unprivileged and does the env/gating/hostname logic; the
|
|
8
|
+
# privileged bits go through root sudo-scripts (start-tailscaled.sh / ts-cli.sh),
|
|
9
|
+
# matching the init-firewall pattern. tailscaled runs as root in userspace
|
|
10
|
+
# networking mode — no TUN device needed, and root is what lets Tailscale SSH open
|
|
11
|
+
# login sessions.
|
|
12
|
+
#
|
|
13
|
+
# Config — all via env. The natural place to set these is the per-project env
|
|
14
|
+
# file ~/.devs/envs/<project>/.env on the host, which devs mounts at
|
|
15
|
+
# /home/node/.devs-env/.env and which this script sources below:
|
|
16
|
+
# TS_ENABLE master switch, STRICTLY OPT-IN. Off unless set to
|
|
17
|
+
# "1"/"true"/"yes". A key alone does NOT enable Tailscale, so an
|
|
18
|
+
# auth key can live in the shared default .env harmlessly until a
|
|
19
|
+
# given project sets TS_ENABLE=1.
|
|
20
|
+
# TS_AUTHKEY PREFERRED key: a reusable, tagged, NON-ephemeral key from the
|
|
21
|
+
# Tailscale admin console. Non-ephemeral is deliberate — the node
|
|
22
|
+
# survives going offline (WAN blip / host sleep) and just reconnects,
|
|
23
|
+
# so you can rely on SSH-ing into it. (An *ephemeral* node is instead
|
|
24
|
+
# garbage-collected by the control plane shortly after it disconnects;
|
|
25
|
+
# if that happens while the container keeps running, tailscaled wedges
|
|
26
|
+
# retrying a dead node key — "PollNetMap: initial fetch failed 404:
|
|
27
|
+
# node not found" — until the next bring-up.)
|
|
28
|
+
# TS_AUTHKEY_EPHEMERAL
|
|
29
|
+
# FALLBACK key, used only when TS_AUTHKEY is unset: a reusable,
|
|
30
|
+
# tagged, EPHEMERAL key. Gives a throwaway node that auto-cleans when
|
|
31
|
+
# the container goes away (no stale-node clutter), accepting the
|
|
32
|
+
# mid-life-GC caveat above. Natural home: the shared default .env, so
|
|
33
|
+
# any container still joins the tailnet even without a per-project
|
|
34
|
+
# stable key. With NEITHER set (and TS_ENABLE=1) => warn + exit 0.
|
|
35
|
+
# TS_HOSTNAME tailnet hostname. Default: <DEVS_PROJECT_NAME>-<DEVCONTAINER_NAME>
|
|
36
|
+
# (e.g. myorg-myapp-alice) -> <name>.<tailnet>.ts.net.
|
|
37
|
+
# Dev name alone isn't unique across projects, hence the prefix.
|
|
38
|
+
# TS_TAGS advertised tags (default: tag:devcontainer). Must match the
|
|
39
|
+
# tagOwners in your tailnet ACL and the auth key's tag.
|
|
40
|
+
# TS_SERVE_PORT if set, serve this local port over HTTPS on the node's name.
|
|
41
|
+
# TS_FUNNEL "1"/"true": expose TS_SERVE_PORT to the PUBLIC internet
|
|
42
|
+
# (Funnel) instead of tailnet-only serve.
|
|
43
|
+
# TS_SSH "1"/"true": accept Tailscale SSH into the container (needs an
|
|
44
|
+
# `ssh` rule in the tailnet ACL allowing the `node` user).
|
|
45
|
+
set -uo pipefail
|
|
46
|
+
|
|
47
|
+
# Pull in secrets/config from the mounted devs env file, if present.
|
|
48
|
+
if [ -f /home/node/.devs-env/.env ]; then
|
|
49
|
+
set -a; . /home/node/.devs-env/.env; set +a
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Master switch — strictly opt-in, off by default (like the firewall). A key
|
|
53
|
+
# alone does NOT enable it, so a tailnet auth key can sit in the shared default
|
|
54
|
+
# .env harmlessly until a project explicitly sets TS_ENABLE=1.
|
|
55
|
+
case "${TS_ENABLE:-}" in
|
|
56
|
+
1|true|yes) ;;
|
|
57
|
+
*)
|
|
58
|
+
echo "ℹ️ Tailscale not enabled (TS_ENABLE=${TS_ENABLE:-unset}) — skipping."
|
|
59
|
+
echo " Set TS_ENABLE=1 (+ TS_AUTHKEY) to join the tailnet."
|
|
60
|
+
exit 0 ;;
|
|
61
|
+
esac
|
|
62
|
+
|
|
63
|
+
# Pick the auth key: prefer the stable (non-ephemeral) TS_AUTHKEY; fall back to an
|
|
64
|
+
# ephemeral throwaway key. KEY_MODE is just for logging — the node's actual
|
|
65
|
+
# ephemerality is baked into whichever key was minted, not set here.
|
|
66
|
+
if [ -n "${TS_AUTHKEY:-}" ]; then
|
|
67
|
+
AUTHKEY="$TS_AUTHKEY"; KEY_MODE="stable (non-ephemeral)"
|
|
68
|
+
elif [ -n "${TS_AUTHKEY_EPHEMERAL:-}" ]; then
|
|
69
|
+
AUTHKEY="$TS_AUTHKEY_EPHEMERAL"; KEY_MODE="ephemeral (fallback)"
|
|
70
|
+
else
|
|
71
|
+
echo "⚠️ TS_ENABLE is set but no key found — skipping Tailscale."
|
|
72
|
+
echo " Set TS_AUTHKEY (reusable, tagged, non-ephemeral) for a stable node,"
|
|
73
|
+
echo " or TS_AUTHKEY_EPHEMERAL (reusable, tagged, ephemeral) for a throwaway one,"
|
|
74
|
+
echo " in a mounted .env (e.g. ~/.devs/envs/default/.env)."
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
SSH_ON=""
|
|
79
|
+
case "${TS_SSH:-}" in 1|true|yes) SSH_ON=1 ;; esac
|
|
80
|
+
|
|
81
|
+
# tailscale CLI against the root daemon, via the NOPASSWD sudo-script.
|
|
82
|
+
TS=(sudo -n /usr/local/bin/ts-cli.sh)
|
|
83
|
+
|
|
84
|
+
# Derive a DNS-safe hostname. Prefer an explicit override; else <project>-<dev>
|
|
85
|
+
# (unique across projects, stable in both live and copy modes); else fall back.
|
|
86
|
+
if [ -n "${TS_HOSTNAME:-}" ]; then
|
|
87
|
+
RAW_NAME="$TS_HOSTNAME"
|
|
88
|
+
elif [ -n "${DEVS_PROJECT_NAME:-}" ] && [ -n "${DEVCONTAINER_NAME:-}" ]; then
|
|
89
|
+
RAW_NAME="${DEVS_PROJECT_NAME}-${DEVCONTAINER_NAME}"
|
|
90
|
+
else
|
|
91
|
+
RAW_NAME="${WORKSPACE_FOLDER_NAME:-${DEVCONTAINER_NAME:-devcontainer}}"
|
|
92
|
+
fi
|
|
93
|
+
HOSTNAME_TS=$(printf '%s' "$RAW_NAME" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]-')
|
|
94
|
+
HOSTNAME_TS="${HOSTNAME_TS:-devcontainer}"
|
|
95
|
+
# Namespace every devs node under a single `devs-` prefix so one `Host devs-*`
|
|
96
|
+
# block in ~/.ssh/config covers all repos with this devcontainer structure.
|
|
97
|
+
# Idempotent: strip any existing prefix first so we never get `devs-devs-`.
|
|
98
|
+
HOSTNAME_TS="devs-${HOSTNAME_TS#devs-}"
|
|
99
|
+
TAGS="${TS_TAGS:-tag:devcontainer}"
|
|
100
|
+
|
|
101
|
+
# Start the daemon (root, detached, idempotent) if not already up.
|
|
102
|
+
echo "🔌 Ensuring tailscaled is running (userspace networking, root)…"
|
|
103
|
+
sudo -n /usr/local/bin/start-tailscaled.sh
|
|
104
|
+
|
|
105
|
+
up_args=(--authkey="$AUTHKEY" --hostname="$HOSTNAME_TS"
|
|
106
|
+
--advertise-tags="$TAGS" --accept-dns=false --accept-routes=false)
|
|
107
|
+
[ -n "$SSH_ON" ] && up_args+=(--ssh)
|
|
108
|
+
|
|
109
|
+
echo "🔗 tailscale up as '$HOSTNAME_TS' [$KEY_MODE] (tags: $TAGS${SSH_ON:+, ssh})…"
|
|
110
|
+
if ! "${TS[@]}" up "${up_args[@]}"; then
|
|
111
|
+
echo "⚠️ tailscale up failed — see /var/log/tailscaled.log. Container continues without Tailscale."
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
dnsname=$("${TS[@]}" status --json 2>/dev/null | jq -r '.Self.DNSName // empty' | sed 's/\.$//')
|
|
116
|
+
echo "✅ On tailnet as: ${dnsname:-$HOSTNAME_TS}"
|
|
117
|
+
[ -n "$SSH_ON" ] && echo "🔑 Tailscale SSH on — connect with: ssh node@${dnsname:-$HOSTNAME_TS}"
|
|
118
|
+
|
|
119
|
+
# Writeback handshake: record this node's resolved tailnet identity into the
|
|
120
|
+
# host-shared env mount (a rw bind mount), keyed by our docker container id (the
|
|
121
|
+
# container's $HOSTNAME, which `docker inspect --format '{{.Config.Hostname}}'`
|
|
122
|
+
# returns on the host). Lets `devs vscode` join container -> tailnet node by
|
|
123
|
+
# container id with a cheap `docker inspect` (no `docker exec`) and use the REAL
|
|
124
|
+
# resolved name — immune to tailnet clash-suffixing (…-eamonn-1) and TS_HOSTNAME
|
|
125
|
+
# overrides. Pruned by `devs clean`; stale files are simply never read.
|
|
126
|
+
if [ -n "${dnsname:-}" ] && [ -d /home/node/.devs-env ]; then
|
|
127
|
+
cid="${HOSTNAME:-$(hostname 2>/dev/null)}"
|
|
128
|
+
ssh_flag=false; [ -n "$SSH_ON" ] && ssh_flag=true
|
|
129
|
+
nodes_dir=/home/node/.devs-env/ts-nodes
|
|
130
|
+
if mkdir -p "$nodes_dir" 2>/dev/null && \
|
|
131
|
+
jq -n --arg dnsname "$dnsname" --arg short "${dnsname%%.*}" \
|
|
132
|
+
--arg container_id "$cid" --arg dev "${DEVCONTAINER_NAME:-}" \
|
|
133
|
+
--argjson ssh "$ssh_flag" \
|
|
134
|
+
'{dnsname:$dnsname, short:$short, container_id:$container_id, dev:$dev, ssh:$ssh}' \
|
|
135
|
+
> "$nodes_dir/$cid.json" 2>/dev/null; then
|
|
136
|
+
echo "📝 Tailnet handshake → .devs-env/ts-nodes/$cid.json"
|
|
137
|
+
else
|
|
138
|
+
echo "⚠️ Could not write tailnet handshake file (continuing)."
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# Optionally publish a port.
|
|
143
|
+
if [ -n "${TS_SERVE_PORT:-}" ]; then
|
|
144
|
+
case "${TS_FUNNEL:-}" in
|
|
145
|
+
1|true|yes)
|
|
146
|
+
echo "🌐 funnel (PUBLIC internet) → :$TS_SERVE_PORT"
|
|
147
|
+
"${TS[@]}" funnel --bg "$TS_SERVE_PORT" \
|
|
148
|
+
|| echo "⚠️ funnel failed (needs Funnel enabled + the funnel node attribute in ACL)"
|
|
149
|
+
;;
|
|
150
|
+
*)
|
|
151
|
+
echo "🔒 serve (tailnet only) → :$TS_SERVE_PORT"
|
|
152
|
+
"${TS[@]}" serve --bg "$TS_SERVE_PORT" \
|
|
153
|
+
|| echo "⚠️ serve failed (needs HTTPS certificates enabled on the tailnet)"
|
|
154
|
+
;;
|
|
155
|
+
esac
|
|
156
|
+
"${TS[@]}" serve status 2>/dev/null || true
|
|
157
|
+
fi
|
|
158
|
+
exit 0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Root helper (NOPASSWD sudo-script): make the container resolve the Tailscale
|
|
3
|
+
# split-DNS sample-app hostnames by prepending the MagicDNS resolver
|
|
4
|
+
# (100.100.100.100) to /etc/resolv.conf. See the "Split-DNS / MagicDNS" section
|
|
5
|
+
# of the devs Tailscale docs (docs/tailscale-setup.md) for the why.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
RESOLV=/etc/resolv.conf
|
|
9
|
+
MAGICDNS=100.100.100.100
|
|
10
|
+
SOCK=/var/run/tailscale/tailscaled.sock
|
|
11
|
+
|
|
12
|
+
# No-op unless Tailscale is actually up — otherwise we'd point the resolver at an
|
|
13
|
+
# unreachable server and stall every lookup behind a timeout.
|
|
14
|
+
if [ ! -S "$SOCK" ] || ! /usr/local/bin/tailscale --socket="$SOCK" status >/dev/null 2>&1; then
|
|
15
|
+
echo "ℹ️ Tailscale not up — skipping MagicDNS resolv.conf setup."
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Already prepended? (an uncommented `nameserver 100.100.100.100` line)
|
|
20
|
+
if grep -qE "^[[:space:]]*nameserver[[:space:]]+${MAGICDNS}([[:space:]]|\$)" "$RESOLV"; then
|
|
21
|
+
echo "✅ MagicDNS resolver already present in $RESOLV — nothing to do."
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Prepend as the first nameserver. glibc reads nameserver lines in order and, on
|
|
26
|
+
# MagicDNS SERVFAIL for non-tailnet names, falls through to the ISP servers that
|
|
27
|
+
# remain below. /etc/resolv.conf is a Docker bind mount, so we rewrite it in place
|
|
28
|
+
# (truncate + write) rather than mv'ing a new file over the mounted inode.
|
|
29
|
+
new=$({ printf 'nameserver %s\n' "$MAGICDNS"; cat "$RESOLV"; })
|
|
30
|
+
printf '%s\n' "$new" > "$RESOLV"
|
|
31
|
+
echo "✅ Prepended MagicDNS resolver ($MAGICDNS) to $RESOLV for tailnet split-DNS."
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Root helper (NOPASSWD sudo-script): start tailscaled as a detached daemon if it
|
|
3
|
+
# isn't already running. Runs as root so Tailscale SSH can open login sessions;
|
|
4
|
+
# uses userspace networking so no TUN device is required. Idempotent.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
SOCK="/var/run/tailscale/tailscaled.sock"
|
|
8
|
+
|
|
9
|
+
# Idempotency / self-heal. Only skip if a tailscaled is ACTUALLY running. A plain
|
|
10
|
+
# container restart (host reboot, `docker start`) re-runs only PID 1 — the daemon
|
|
11
|
+
# process is gone, but its socket file persists on the container's writable layer.
|
|
12
|
+
# A naive "socket exists => already up" check then sees that stale socket, skips
|
|
13
|
+
# startup, and every subsequent `tailscale up` fails with "failed to connect to
|
|
14
|
+
# local tailscaled". So probe for a live daemon; if the socket is stale, remove it
|
|
15
|
+
# and start fresh.
|
|
16
|
+
if [ -S "$SOCK" ]; then
|
|
17
|
+
if pgrep -x tailscaled >/dev/null 2>&1; then
|
|
18
|
+
exit 0 # a real daemon is running — nothing to do
|
|
19
|
+
fi
|
|
20
|
+
echo "Removing stale tailscaled socket (no tailscaled process is running)…"
|
|
21
|
+
rm -f "$SOCK"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# setsid + </dev/null detaches the daemon from the calling exec session so it
|
|
25
|
+
# survives after postCreate (or whoever invoked it) exits.
|
|
26
|
+
setsid /usr/local/bin/tailscaled \
|
|
27
|
+
--tun=userspace-networking \
|
|
28
|
+
--socket="$SOCK" \
|
|
29
|
+
--statedir=/var/lib/tailscale \
|
|
30
|
+
</dev/null >>/var/log/tailscaled.log 2>&1 &
|
|
31
|
+
disown 2>/dev/null || true
|
|
32
|
+
|
|
33
|
+
for _ in $(seq 1 30); do
|
|
34
|
+
[ -S "$SOCK" ] && break
|
|
35
|
+
sleep 0.5
|
|
36
|
+
done
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Root helper (NOPASSWD sudo-script): run the tailscale CLI against the container's
|
|
3
|
+
# root daemon socket. The node-user `ts` wrapper calls this; start-tailscale.sh
|
|
4
|
+
# uses it for up/serve/status.
|
|
5
|
+
exec /usr/local/bin/tailscale --socket=/var/run/tailscale/tailscaled.sock "$@"
|
|
@@ -54,6 +54,9 @@ def prepare_devcontainer_environment(
|
|
|
54
54
|
'DEVCONTAINER_NAME': dev_name,
|
|
55
55
|
'GIT_REMOTE_URL': git_remote_url,
|
|
56
56
|
'WORKSPACE_FOLDER_NAME': workspace_folder_name,
|
|
57
|
+
# org-repo form (e.g. "myorg-myapp"); used to build a tailnet
|
|
58
|
+
# hostname like <project>-<dev>, stable across live/copy modes.
|
|
59
|
+
'DEVS_PROJECT_NAME': project_name,
|
|
57
60
|
})
|
|
58
61
|
|
|
59
62
|
# Set environment mount path (uses project-specific if exists, else default)
|
|
@@ -15,7 +15,11 @@ devs_common/core/container.py
|
|
|
15
15
|
devs_common/core/project.py
|
|
16
16
|
devs_common/core/workspace.py
|
|
17
17
|
devs_common/templates/Dockerfile
|
|
18
|
+
devs_common/templates/README-vscode-settings.md
|
|
18
19
|
devs_common/templates/devcontainer.json
|
|
20
|
+
devs_common/templates/machine-settings.json
|
|
21
|
+
devs_common/templates/machine-settings.tmux.json
|
|
22
|
+
devs_common/templates/register-vscode-extensions.js
|
|
19
23
|
devs_common/templates/vscode-settings-reference.md
|
|
20
24
|
devs_common/templates/extensions/README.md
|
|
21
25
|
devs_common/templates/extensions/devs-bridge-drop.vsix
|
|
@@ -23,16 +27,23 @@ devs_common/templates/extensions/install.sh
|
|
|
23
27
|
devs_common/templates/scripts/copy-ssh.sh
|
|
24
28
|
devs_common/templates/scripts/install-bridge-drop.sh
|
|
25
29
|
devs_common/templates/scripts/post-create-wrapper.sh
|
|
30
|
+
devs_common/templates/scripts/post-start.sh
|
|
26
31
|
devs_common/templates/scripts/python-venv-integration.sh
|
|
27
32
|
devs_common/templates/scripts/screenshot-webapp.sh
|
|
33
|
+
devs_common/templates/scripts/setup-vscode-settings.sh
|
|
28
34
|
devs_common/templates/scripts/setup-workspace.sh
|
|
35
|
+
devs_common/templates/scripts/start-tailscale.sh
|
|
29
36
|
devs_common/templates/scripts/start-tunnel.sh
|
|
37
|
+
devs_common/templates/scripts/ts.sh
|
|
30
38
|
devs_common/templates/scripts/tunnel-status.sh
|
|
31
39
|
devs_common/templates/scripts/webapp-screenshot.py
|
|
32
40
|
devs_common/templates/sudo-scripts/init-firewall.sh
|
|
33
41
|
devs_common/templates/sudo-scripts/setup-devs-env.sh
|
|
42
|
+
devs_common/templates/sudo-scripts/setup-magicdns.sh
|
|
34
43
|
devs_common/templates/sudo-scripts/setup-vscode-cache.sh
|
|
35
44
|
devs_common/templates/sudo-scripts/start-services.sh
|
|
45
|
+
devs_common/templates/sudo-scripts/start-tailscaled.sh
|
|
46
|
+
devs_common/templates/sudo-scripts/ts-cli.sh
|
|
36
47
|
devs_common/utils/__init__.py
|
|
37
48
|
devs_common/utils/config_hash.py
|
|
38
49
|
devs_common/utils/console.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/install-bridge-drop.sh
RENAMED
|
File without changes
|
{devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/scripts/python-venv-integration.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/sudo-scripts/setup-devs-env.sh
RENAMED
|
File without changes
|
{devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh
RENAMED
|
File without changes
|
{devs_common-4.0.4 → devs_common-5.0.0}/devs_common/templates/sudo-scripts/start-services.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|