feat: --no-proxy mode for app deploy/init/logs/... (#102)#103
Conversation
Specifies a coexisting no-proxy path for app {init,deploy,logs,stop,
restart,status,destroy,env,rollback}, a server-side .conoha-mode
marker for hybrid detection with --proxy/--no-proxy overrides, and
the per-command semantics + exit codes. Absorbs #93 (slot-aware
logs/stop/restart/status in proxy mode).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 bite-sized tasks covering cmd/app/mode.go foundation, per-command mode dispatch (init, deploy, rollback, destroy, logs/stop/restart/ status, env warning shim), documentation, and verification. Each task follows TDD (failing test → implementation → passing test → commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce Mode enum (proxy | no-proxy), ErrNoMarker / ErrModeConflict, shell-command builders for the .conoha-mode marker file, ReadMarker / WriteMarker / ResolveMode / ReadCurrentSlot helpers, and the --proxy/--no-proxy mutually-exclusive flag pair. Foundation for #102. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No-proxy init installs only the mkdir + marker write (no conoha.yml required). Proxy init continues through the existing upsert path and now writes the marker at the end. --app-name is required with --no-proxy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The .conoha-mode marker is the single source of truth for mode dispatch in subsequent app commands. A silently-skipped marker leaves deploy/logs/stop/... unable to detect mode and surfaces a misleading 'not initialized' error for an app that was successfully registered with the proxy. Match no-proxy's fatal-on-marker-failure policy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
runDeployDispatch reads the --no-proxy flag, resolves the server marker, and either calls runProxyDeploy (existing blue/green flow) or runNoProxyDeploy (tar upload to /opt/conoha/<name>/ + compose up against the project name <name>). Proxy/no-proxy marker mismatches produce the standard mode-conflict error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec §3.2 requires rejecting 'conoha app deploy <server>' (no flag) or '--proxy' against an app with a no-proxy marker. Previously only the --no-proxy branch enforced marker consistency; the proxy branch went straight to admin.Get and returned a misleading 'service not found'. Now the proxy branch reads the marker first and issues the standard mode-conflict error. Also unify the 'not initialized' phrasing across both branches. Additionally, single-quote composeFile in buildNoProxyDeployCmd so the builder stays safe if a future caller passes a non-whitelisted compose file path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
--no-proxy flag (or a no-proxy marker detected after SSH connect) now returns an explicit error pointing at 'git checkout <rev> && conoha app deploy --no-proxy'. Proxy-mode behavior is unchanged but now runs through ResolveMode first so marker mismatches surface as the standard mode-conflict error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
destroy now reads the .conoha-mode marker before the destroy script runs (the script removes the marker via rm -rf /opt/conoha/<app>) and only deregisters from conoha-proxy when the marker is 'proxy'. No-proxy and unmarked (legacy v0.1.x) servers continue to run the shared compose-down + rm -rf cleanup without touching the proxy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Absorbs #93 for app logs: proxy mode reads CURRENT_SLOT and runs 'docker compose -p <app>-<slot> logs' against the active slot project. No-proxy mode keeps the flat 'cd /opt/conoha/<app>' path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Proxy-mode stop runs 'docker compose -p <app>-<slot> stop' against the active slot (CURRENT_SLOT); no-proxy keeps the legacy flat path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Proxy-mode restart runs 'docker compose -p <app>-<slot> restart' against CURRENT_SLOT. No-proxy keeps the legacy flat path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Proxy status scans all slot compose projects and appends the proxy service phase block. No-proxy status runs a simple 'docker compose ps' in the flat work dir and skips the proxy enrichment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ReadCurrentSlot returned raw trimmed file content, which was then interpolated into 'docker compose -p <app>-<slot>' in logs/stop/ restart. A compromised or manually-edited CURRENT_SLOT could smuggle shell metacharacters through that path. Re-apply ValidateSlotID (same rule that gates writes at deploy time) so the read side is defense-in-depth rather than trust-the-file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
app env still writes to /opt/conoha/<app>.env.server (the v0.1.x path, now canonical for no-proxy mode). When the .conoha-mode marker says proxy, print a single-line warning pointing at #94 rather than breaking existing CI scripts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 'Two deploy modes' section in README.md (ja), README-en.md, and README-ko.md; a full no-proxy single-server recipe at recipes/single-server-app-noproxy.md; and a one-line cross-reference from the 2026-04-20 proxy-deploy spec to the new 2026-04-21 no-proxy design spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix 1 — app init now rejects implicit mode switches: Before writing the marker, init reads the existing marker and returns the standard mode-conflict error if it disagrees with the target mode. Previously, 'conoha app init --no-proxy' on a proxy-registered app (or vice versa) would silently overwrite the marker while leaving the proxy service or flat layout behind, defeating spec §2.3's coexistence guarantee. Fix 2 — no-proxy deploy now merges /opt/conoha/<app>.env.server: Spec §3.2 requires replaying the v0.1.x behavior where values set by 'conoha app env set' are merged into the work-dir .env at deploy time. buildNoProxyDeployCmd now prepends a shell block that cats the env.server file into .env (server-side values first so user's repo-level .env wins on duplicates per last-occurrence semantics) before 'docker compose up'. Added test assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adversarial review (second pass) — 5 defects found, blocks mergeSecond review after the earlier round. Spec + plan + tests all checked. Critical (blocks merge)C1 — `app env unset` silently fails to take effect on redeploy.
Fix: rebuild `.env` from scratch per deploy (don't cat the existing `.env`). C2 — `.env` merge precedence is wrong for secret injection. Fix: swap order so `.env.server` wins; document in README. ImportantI1 — Zero test coverage on every dispatch path introduced by this PR. I2 — `destroy` skips proxy DELETE for legacy proxy apps missing the marker. Fix: default to ModeProxy when marker absent + `conoha.yml` validates, or print a warning so users run manual cleanup. I3 — `destroy`/`stop` prompt before mode check. I5 — `maybeWarnProxyEnvMode` adds an SSH round-trip to every `app env get`/`list`. I6 — `status.go:47-49` masks real errors as warnings. MinorM1 — Four different error phrasings for "not initialized"/"not deployed" across init/deploy/rollback/logs/stop/restart/status. Pull `notInitializedError`/`notDeployedError` helpers into `mode.go`. M2 — `formatModeConflictError` emits literal `` token. Thread the real `serverID`. M4 — `ReadCurrentSlot` error embeds the full (possibly adversarial) slot content via `%q`. Truncate to 16 chars. M5 — No-proxy deploy (`mkdir -p; tar xzf -`) never removes files deleted from the repo. Document. M6 — TOCTOU between marker read and action. Concurrent destroy + deploy can leave a running compose project without a marker. Single-operator assumption is fine; worth a sentence in spec §12. M7 — `runProxyDeploy` open-codes marker read instead of calling `ResolveMode`. Two divergent paths for the same logic. ObservationsO1 — Squash the 5 fix commits (`db7b1b9`, `0281f2f`, `c80d0e6`, `4ff6b2a`) before merge. `git bisect` will land on broken intermediates otherwise. O2 — Spec §12 trade-off drift: §12 said "marker write failure in init → warn only", but `db7b1b9` made it fatal. Update §12. O3 — Spec §2.3 ("no auto-conversion") holds. No silent flip path found. O4 — Spec §11 acceptance "unit tests cover all branches" NOT satisfied — see I1. O5 — logs/stop/restart/status omit `-p ` and rely on cwd-derived project name. Works for lowercase; `ValidateAppName` permits uppercase which Compose normalizes differently. Follow-up. OverallDo not merge as-is. C1/C2 are silent wrong-env deployments. I1 leaves the dispatch logic (the value of this PR) unverified. I2 regresses legacy proxy destroys. I3 creates confusing post-prompt UX. Fix C1, C2, I1, I2, I3 before merge. Squash per O1. |
…tests C1 — app env unset now takes effect on redeploy. buildNoProxyUploadCmd now removes the previous deploy's merged .env before tar extraction so the repo's .env (or its absence) is authoritative each cycle. Subsequent .env.server overlay rebuilds from scratch instead of accumulating stale entries. C2 — .env merge precedence reversed so app env wins over repo .env. .env.server is now appended AFTER the repo .env, making its values last-occurrence and therefore the ones docker compose picks up. This matches v0.1.x ENV_EXISTS semantics: runtime secrets set via conoha app env set override anything committed to the repo. I1 — Extracted resolveModeLogic as a pure function and added an 11-case table-driven test covering every combination of (flag × marker × SSH error). Previously ResolveMode had zero coverage despite being the dispatch spine of this feature. I2 — destroy now honors legacy proxy deployments. When the marker is absent but conoha.yml validates locally, we treat the server as a pre-PR proxy deployment and still issue proxy DELETE to avoid leaking orphan service registrations. I3 — destroy and stop now resolve mode BEFORE prompting. A flag/marker conflict or a "not deployed" error now aborts before asking the user to confirm, removing the "did it or didn't it?" UX after a rejected operation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review fixes — C1/C2/I1/I2/I3 (`b38b4b8`)C1 — `buildNoProxyUploadCmd` now does `rm -f .env` before tar extract, so each deploy rebuilds `.env` from scratch. `app env unset` takes effect on the next deploy. C2 — `.env.server` is now appended after the repo `.env`, so `conoha app env set` values win (last-occurrence in compose parser). I1 — Extracted `resolveModeLogic` as a pure function and added an 11-case table test covering every combination of (`want` × `got` × `readErr`). Previously ResolveMode had zero coverage. I2 — `destroy` now treats marker-absent + locally-valid `conoha.yml` as a legacy proxy deployment and still calls `admin.Delete`. Emits a visible "treating as legacy proxy deployment" notice. I3 — `destroy` and `stop` now resolve mode BEFORE prompting. A flag/marker conflict or "not deployed" error aborts before the user confirms — no more ambiguous post-prompt rejection. Tests green with `-race`. Remaining items from the review (I5, I6, M1–M7, O1 squash, O2 spec update, O5 ValidateAppName lowercase) are follow-ups to land separately. |
* docs(readme): refresh deploy-mode coverage — document proxy + no-proxy in parallel Expands the two-mode summary table with conoha.yml / proxy boot / DNS columns so users can pick a mode at a glance. Adds a parallel "no-proxy mode" subsection that previously only existed as a one-line footnote, plus explicit mode-marker semantics (set by init, auto-detected, --proxy / --no-proxy as override-with-error-on-mismatch). Also documents the full conoha.yml schema (compose_file, accessories, health, deploy.drain_ms) — previously only name/hosts/web were shown. Adds a flags reference table covering --slot, --drain-ms, --follow, --service, --tail, --data-dir that were reachable only via --help. No functional change; just documents features already shipped in #98 (proxy blue/green) and #102/#103 (--no-proxy mode). * docs(readme-en): mirror proxy + no-proxy refresh from README.md * docs(readme-ko): mirror proxy + no-proxy refresh from README.md * docs(readme): fix C1/I1/I2/I3 from doc review - C1: correct .env.server semantics — the local-file auto-copy claim was wrong. `.env.server` is the server-side file at /opt/conoha/<app>.env.server, appended to the deploy's .env after the repo-committed .env (see cmd/app/deploy.go:132-140). - I1: flag-table row for --proxy/--no-proxy wrongly excluded `init`, which also takes the flag (selects the mode to write into the marker). See cmd/app/init.go:25. - I2: rollback on no-proxy emits a dedicated "rollback is not supported" error, not a mode-mismatch error. See cmd/app/rollback.go:22-26. - I3: drop redundant --app-name from proxy-mode init/deploy/rollback examples (proxy mode reads name from conoha.yml's `name` field; --app-name is silently ignored there). Tighten the flag-table row to distinguish where --app-name is actually required vs where it's overridden by conoha.yml. * docs(plan): fix .env.server claim in phase A+B replacement blocks The plan's replacement Markdown for Tasks 2/3/4 (JA/EN/KO READMEs) and the Phase B Task 7 recipe block embedded the same wrong "local .env.server auto-copies to .env on deploy" claim. Ground truth from cmd/app/deploy.go:107-140 and cmd/app/env.go:94,144,170,211: the server-side /opt/conoha/<app>.env.server file (written by `conoha app env set`) is appended onto the repo-committed .env at deploy time; no local .env.server is ever picked up specially. Plan updated so future re-runs don't regress the README. * docs(readme): fix C1/C2/I1/I2 from review — no-proxy init does not install Docker, env is no-proxy only, drain_ms default 30000, flag scope excludes list Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plan): propagate C1/C2/I1/I2 corrections into plan replacement blocks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: t-kim <t-kim@planitai.co.jp> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#123) - Replace the marker-write rollback bullet: PR #103 flipped the policy to fatal in cmd/app/init.go; §12 kept the pre-flip text. Now points to review item I1 and commit db7b1b9 (closes #112). - Add a TOCTOU bullet covering concurrent destroy/deploy against the same app. No code change — single-operator assumption retained (closes #113). Co-authored-by: t-kim <t-kim@planitai.co.jp> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#101) (#135) * feat(ssh): verify host keys via known_hosts + TOFU fallback (closes #101) Every SSH connection the CLI makes used ssh.InsecureIgnoreHostKey(), which was justified in v0.1.x as "personal VPS use" but becomes unsafe in the post-#98/#103 world: the same SSH channel now carries state-mutating Admin API calls (upsert, deploy, delete), compose archive uploads, and a growing number of ops commands. A MITM on the path can silently reroute those to an attacker-controlled responder and misreport phase/active_target back to the operator. New default: - Resolve ~/.ssh/known_hosts (override: SSH_KNOWN_HOSTS env var). - On first connect to an unknown host, prompt operator to accept and pin the key (TOFU). - When CONOHA_NO_INPUT is set or stdin isn't a TTY, refuse the connection with a message pointing at --insecure and ssh-keyscan. - On host-key mismatch (pinned key differs from what the server presented), return HostKeyMismatchError with a recovery hint ('ssh-keygen -R <host>' to remove the stale pin after a legit rebuild). Opt-out: - --insecure global flag (persistent, applies to every subcommand). - CONOHA_SSH_INSECURE env var for CI / wrappers. - Preserves the old behavior bit-for-bit when set; documented as the explicit lab / throwaway-VPS knob. Connect() reads insecure state from either ConnectConfig.Insecure or the env, so no caller changes were needed across the 7 existing Connect sites. Tests: - Insecure callback accepts any key. - Pre-seeded known_hosts + mismatched server key → HostKeyMismatchError with the 'ssh-keygen -R' hint. - Unknown host + noInput → helpful refusal message. - Missing known_hosts is auto-created. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lint): satisfy errcheck on sshClient.Close + collapse S1020 in known_hosts - cmd/gpu/setup.go: explicit `_ =` discard on Close() (errcheck does not exclude *ssh.Client; the io.Closer exclusion only matches the interface type itself). - internal/ssh/knownhosts.go: drop redundant `remote != nil` outer guard; type assertion on a nil interface returns ok=false. (staticcheck S1020.) * refactor(ssh): drop dead Insecure field, alias config import, fail-closed on non-TTY TOFU Three follow-ups from PR #135 review: 1. Remove ConnectConfig.Insecure — no caller sets it. The --insecure flag / CONOHA_SSH_INSECURE env var routes through configpkg.IsSSHInsecure inside Connect, which is the single source of truth. The unused field was a misleading second knob. 2. Alias the config import as `configpkg` to remove the package-vs-local shadowing trap in Connect (the previous `config -> clientCfg` rename only fixed the immediate collision; an import alias prevents recurrence). 3. Guard TOFU prompt with term.IsTerminal so a non-TTY stdin (CI, build script piping a heredoc) fails closed instead of letting an attacker- controlled `yes\n` silently trust an unknown host. The function-level doc already promised this behavior. Adds TestHostKeyCallback_UnknownHost_NonTTYFailsClosed; skips when stdin happens to be a real TTY (interactive `go test` runs). --------- Co-authored-by: t-kim <t-kim@planitai.co.jp> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds a coexisting
--no-proxymode to theconoha app *command tree. Closes #102. Absorbs #93 (slot-aware logs/stop/restart/status in proxy mode).Spec: `docs/superpowers/specs/2026-04-21-no-proxy-mode-design.md`
Plan: `docs/superpowers/plans/2026-04-21-no-proxy-mode.md`
Design
/opt/conoha/<name>/.conoha-mode(written byapp initon either path) with--proxy/--no-proxymutually-exclusive flag override./opt/conoha/.app initnow rejects implicit mode switches./opt/conoha/<app>.env.server(written byapp env set) into work-dir.envbeforedocker compose up.logs/stop/restart/statusnow target the active slot in proxy mode viaCURRENT_SLOT.Command matrix
app initapp deploy/opt/conoha/<app>/+ compose upapp rollback/rollbackapp destroyapp logs/stop/restart/statusapp envBreaking changes
None. Proxy-mode users who ran
app initbefore this PR will be prompted to re-run init once (their marker file doesn't exist yet).Test plan
Follow-ups (non-blocking)
conoha app resetfor blue/green model #92 `app reset` reintroduction — needs both modes, blocked on this PR.🤖 Generated with Claude Code