Skip to content

feat: Smart online installer — auto-detect RVSconfig.yml, URL/paste/wizard#8

Open
PhilippWu wants to merge 1 commit into
mainfrom
feat/4-smart-installer
Open

feat: Smart online installer — auto-detect RVSconfig.yml, URL/paste/wizard#8
PhilippWu wants to merge 1 commit into
mainfrom
feat/4-smart-installer

Conversation

@PhilippWu
Copy link
Copy Markdown
Owner

Closes #4

Summary

Replaces the single mandatory YAML-paste step in install.sh with a smart four-path config resolution chain. The installer always picks the fastest available path automatically — no flags or arguments needed.

Config resolution priority

curl -fsSL .../install.sh | bash
         │
         ├─ RVSconfig.yml on disk? ──────────────────────────────► use it  (Path 1)
         │
         └─ single prompt ──────────────────────────────────────────────────────
                    │
                    ├─ URL (https://…) entered ──► curl + validate ──► use it  (Path 2)
                    │
                    ├─ YAML content pasted, Ctrl-D ──────────────────► use it  (Path 3)
                    │
                    └─ Enter (empty) ──► embedded wizard ──► write file ─► use it  (Path 4)

Path 1 — file on disk (zero prompts)

If /etc/dev-server/RVSconfig.yml or ./RVSconfig.yml exists the installer uses it directly. Enables fully automated re-runs and SCP-based pre-provisioning.

Path 2 — URL download

User pastes an https:// URL at the prompt (private Gist, object-storage bucket, etc.). Installer downloads with curl --max-time 60, validates, proceeds.

Path 3 — YAML paste (preserved from before)

User pastes the full RVSconfig.yml content directly into the terminal and presses Ctrl-D. This is the previous default behavior, now the third option rather than the only one.

Path 4 — embedded Bash wizard

Empty Enter at the prompt launches a four-step interactive wizard:

  • Step 1/4 — Network: domain (or IP-only), subdomain, email, optional Cloudflare DNS
  • Step 2/4 — Coder admin password: with auto-generate fallback
  • Step 3/4 — AI agents: Copilot, Claude, Codex, Gemini, OpenCode
  • Step 4/4 — API keys: prompted per enabled agent

The wizard writes RVSconfig.yml to disk before proceeding, so the next run uses Path 1 automatically (idempotent).

Other changes

  • All four paths save the resolved config to /etc/dev-server/RVSconfig.yml (mode 0600) before YAML→env parsing
  • YAML validation helper _validate_yaml() applied consistently across all paths
  • TTY helpers (_read_line, _read_secret, _read_confirm) use exec 3</dev/tty so all interactive I/O works correctly even when stdin is a pipe (curl | bash)
  • Secret inputs (_read_secret) save/restore full stty settings
  • Script header comment updated to document all four paths

Bug fixes (inherited from main)

  • configurator/validators.py: restored missing def validate_api_key_optional() declaration (orphaned function body)
  • configurator/tests/test_validators.py: added coder_admin_password to _full_config() base fixture

Documentation

docs/deployment.mdBare-Server Install section rewritten to document all four paths with examples for each.

Tests

245/245 passed ✅

… embedded wizard

Closes #4

install.sh now resolves RVSconfig.yml through a four-path priority chain
instead of always requiring a manual YAML paste:

  Path 1 — existing file on disk (/etc/dev-server/RVSconfig.yml or ./RVSconfig.yml)
            → used directly, no prompts shown (enables idempotent re-runs)
  Path 2 — URL entered at the prompt (http/https prefix)
            → downloaded with curl, validated, written to disk
  Path 3 — YAML content pasted at the prompt, Ctrl-D to end
            → preserved from previous behaviour
  Path 4 — empty Enter at the prompt
            → launches an embedded Bash wizard (Step 1-4: network/domain,
               admin password, AI agents, API keys); writes RVSconfig.yml
               to disk before proceeding so re-runs use Path 1

All paths persist the resolved config to /etc/dev-server/RVSconfig.yml
(mode 0600) before parsing, making every subsequent run fully automatic.

Also fixes inherited bugs from main:
- validators.py: restore missing 'def validate_api_key_optional' declaration
- test_validators.py: add coder_admin_password to _full_config() base fixture
Copilot AI review requested due to automatic review settings April 10, 2026 13:19
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2dbfcb2b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread install.sh
# We read interactive input from /dev/tty so the user can still paste config.
echo -e "${BOLD}Paste the contents of your RVSconfig.yml below.${RESET}"
echo -e "When done, press ${BOLD}Ctrl-D${RESET} (EOF) on an empty line.\n"
exec 3</dev/tty
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Defer /dev/tty open until interactive input is needed

Opening fd 3 with exec 3</dev/tty at startup makes the script fail immediately in non-interactive contexts (/dev/tty: No such device or address), even when RVSconfig.yml already exists and Path 1 should run with zero prompts. This breaks the new automated re-run/pre-provision flow for cloud-init, CI, or cron executions that lack a controlling TTY.

Useful? React with 👍 / 👎.

Comment thread install.sh
Comment on lines +240 to +244
use_cloudflare="false"
warn "Manual DNS required: after boot, point ${subdomain:-<subdomain>}.${domain:-<domain>} → this server's public IP."
fi
else
ip_only="true"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove unsupported non-Cloudflare/IP-only wizard branches

The wizard now allows use_cloudflare=false and ip_only=true, but the provisioning pipeline still hard-requires DOMAIN, SUBDOMAIN, EMAIL, CLOUDFLARE_API_TOKEN, and CLOUDFLARE_ZONE_ID and always runs DNS setup (dev-server-provision/setup.sh required-vars block and unconditional dns.sh call). Choosing either branch therefore generates a config that predictably aborts later in setup, so these options are currently non-functional.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a smarter install.sh flow that auto-resolves RVSconfig.yml via on-disk detection, URL download, YAML paste, or an interactive wizard—reducing friction for bare-server installs and enabling more automated re-runs.

Changes:

  • Updated install.sh to resolve config via a 4-path priority chain and persist the resolved config to /etc/dev-server/RVSconfig.yml.
  • Expanded bare-server installation documentation to describe all config resolution paths with examples.
  • Fixed/adjusted configurator validation plumbing and tests (restored missing validator symbol; updated preflight fixture).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
install.sh Adds multi-path config resolution, TTY read helpers, YAML validation, and wizard-based config generation.
dev-server-provision/docs/deployment.md Documents the new installer config resolution paths and usage examples.
dev-server-provision/configurator/validators.py Restores validate_api_key_optional() symbol.
dev-server-provision/configurator/tests/test_validators.py Updates the full-config fixture to include coder_admin_password.

Comment thread install.sh
Comment on lines +78 to +83
# TTY helpers
# All interactive I/O goes through /dev/tty (fd 3) so the script works
# correctly even when stdin is a pipe (curl ... | bash).
# ---------------------------------------------------------------------------
# When the script is piped (curl ... | sudo bash), stdin is the curl stream.
# We read interactive input from /dev/tty so the user can still paste config.
echo -e "${BOLD}Paste the contents of your RVSconfig.yml below.${RESET}"
echo -e "When done, press ${BOLD}Ctrl-D${RESET} (EOF) on an empty line.\n"
exec 3</dev/tty

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exec 3</dev/tty is unguarded under set -euo pipefail, so the script will exit immediately when there is no controlling TTY (e.g., cloud-init, CI, or any non-interactive re-run that relies on Path 1 with an on-disk config). Please make opening /dev/tty conditional (only when interactive input is needed) and/or provide a fallback to read from stdin when /dev/tty is unavailable.

Copilot uses AI. Check for mistakes.
Comment thread install.sh
Comment on lines +220 to +248
if _read_confirm " Do you have a domain name for this server? [y/N]: " "n"; then
_read_line " Domain (e.g. example.com): "
domain="$_TTY_LINE"

_read_line " Subdomain (e.g. dev): "
subdomain="$_TTY_LINE"

_read_line " Email (for TLS certificates & alerts): "
email="$_TTY_LINE"

ip_only="false"

if _read_confirm " Use Cloudflare for automatic DNS? [Y/n]: " "y"; then
use_cloudflare="true"
echo -e " ${CYAN}Tip: create a scoped token at https://dash.cloudflare.com/profile/api-tokens${RESET}" >/dev/tty
_read_secret " Cloudflare API Token: "
cloudflare_api_token="$_TTY_SECRET"
_read_line " Cloudflare Zone ID: "
cloudflare_zone_id="$_TTY_LINE"
else
use_cloudflare="false"
warn "Manual DNS required: after boot, point ${subdomain:-<subdomain>}.${domain:-<domain>} → this server's public IP."
fi
else
ip_only="true"
use_cloudflare="false"
echo -e " ${CYAN}IP-only mode — Coder will be served over plain HTTP on port 80.${RESET}" >/dev/tty
fi

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wizard’s IP-only branch generates ip_only: true and leaves domain/subdomain/email empty, but the provisioning pipeline still executes dev-server-provision/setup.sh, which currently hard-requires DOMAIN, SUBDOMAIN, EMAIL, and Cloudflare vars and will abort if they’re missing. Either remove/disable IP-only mode in the installer until the backend supports it, or update the provisioning scripts to handle IP-only mode end-to-end (proxy, DNS, required var validation, and CODER_ACCESS_URL).

Suggested change
if _read_confirm " Do you have a domain name for this server? [y/N]: " "n"; then
_read_line " Domain (e.g. example.com): "
domain="$_TTY_LINE"
_read_line " Subdomain (e.g. dev): "
subdomain="$_TTY_LINE"
_read_line " Email (for TLS certificates & alerts): "
email="$_TTY_LINE"
ip_only="false"
if _read_confirm " Use Cloudflare for automatic DNS? [Y/n]: " "y"; then
use_cloudflare="true"
echo -e " ${CYAN}Tip: create a scoped token at https://dash.cloudflare.com/profile/api-tokens${RESET}" >/dev/tty
_read_secret " Cloudflare API Token: "
cloudflare_api_token="$_TTY_SECRET"
_read_line " Cloudflare Zone ID: "
cloudflare_zone_id="$_TTY_LINE"
else
use_cloudflare="false"
warn "Manual DNS required: after boot, point ${subdomain:-<subdomain>}.${domain:-<domain>} → this server's public IP."
fi
else
ip_only="true"
use_cloudflare="false"
echo -e " ${CYAN}IP-only mode — Coder will be served over plain HTTP on port 80.${RESET}" >/dev/tty
fi
while true; do
if _read_confirm " Do you have a domain name for this server? [y/N]: " "n"; then
_read_line " Domain (e.g. example.com): "
domain="$_TTY_LINE"
_read_line " Subdomain (e.g. dev): "
subdomain="$_TTY_LINE"
_read_line " Email (for TLS certificates & alerts): "
email="$_TTY_LINE"
ip_only="false"
if _read_confirm " Use Cloudflare for automatic DNS? [Y/n]: " "y"; then
use_cloudflare="true"
echo -e " ${CYAN}Tip: create a scoped token at https://dash.cloudflare.com/profile/api-tokens${RESET}" >/dev/tty
_read_secret " Cloudflare API Token: "
cloudflare_api_token="$_TTY_SECRET"
_read_line " Cloudflare Zone ID: "
cloudflare_zone_id="$_TTY_LINE"
else
use_cloudflare="false"
warn "Manual DNS required: after boot, point ${subdomain:-<subdomain>}.${domain:-<domain>} → this server's public IP."
fi
break
fi
warn "IP-only mode is not supported by the current provisioning pipeline. Please provide a domain name and try again."
done

Copilot uses AI. Check for mistakes.
Comment thread install.sh
Comment on lines 414 to 422
# Add derived CODER variables
DOMAIN="$(grep '^DOMAIN=' "$ENV_FILE" | cut -d= -f2-)"
SUBDOMAIN="$(grep '^SUBDOMAIN=' "$ENV_FILE" | cut -d= -f2-)"
IP_ONLY_VAL="$(grep '^IP_ONLY=' "$ENV_FILE" | cut -d= -f2- || true)"
if [[ -n "$DOMAIN" && -n "$SUBDOMAIN" ]]; then
CODER_FQDN="https://${SUBDOMAIN}.${DOMAIN}"
grep -q '^CODER_URL=' "$ENV_FILE" || echo "CODER_URL=${CODER_FQDN}" >> "$ENV_FILE"
grep -q '^CODER_URL=' "$ENV_FILE" || echo "CODER_URL=${CODER_FQDN}" >> "$ENV_FILE"
grep -q '^CODER_ACCESS_URL=' "$ENV_FILE" || echo "CODER_ACCESS_URL=${CODER_FQDN}" >> "$ENV_FILE"
fi
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The derived CODER_FQDN logic reads DOMAIN/SUBDOMAIN by grepping the env file, but values are written with %q. Empty values become '', which is non-empty to [[ -n ... ]], so the script can incorrectly generate CODER_URL=https://''.'' and also miss “missing required values” warnings. Consider sourcing the env file (or normalizing '' back to empty) before checking emptiness / building URLs.

Copilot uses AI. Check for mistakes.
Comment thread install.sh
Comment on lines +361 to +364
elif [[ "$first_line" =~ ^https?:// ]]; then
# ── Path 2: URL download ────────────────────────────────────────────────
_download_config "$first_line"
RVS_SOURCE="url:${first_line}"
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL config downloads currently accept http:// as well as https:// (^https?://). Because the URL may point to a file containing secrets, allowing plaintext HTTP creates a credential exposure risk. Restrict this path to https:// (and optionally reject URLs with embedded credentials / obvious token query params).

Copilot uses AI. Check for mistakes.
Comment thread install.sh
Comment on lines +167 to +176
local url="$1"
log "Downloading config from: $url"
local tmp curl_err
tmp="$(mktemp)"
curl_err="$(mktemp)"
if ! curl -fsSL --max-time 60 "$url" -o "$tmp" 2>"$curl_err"; then
local msg
msg="$(cat "$curl_err" 2>/dev/null || true)"
rm -f "$tmp" "$curl_err"
die "Failed to download config from ${url}${msg:+ — $msg}"
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The installer logs the full config download URL (log "Downloading config from: $url"). If users provide signed URLs or URLs containing tokens/query secrets, those credentials will be written to /var/log/dev-server-provision.log. Consider redacting the query string (or logging only the host/path) to avoid leaking secrets into logs.

Copilot uses AI. Check for mistakes.
Comment thread install.sh
Comment on lines +308 to +316
RVS_CONTENT="# RVSconfig.yml — generated by install.sh wizard
domain: \"${domain}\"
subdomain: \"${subdomain}\"
email: \"${email}\"
cloudflare_api_token: \"${cloudflare_api_token}\"
cloudflare_zone_id: \"${cloudflare_zone_id}\"
coder_admin_password: \"${coder_admin_password}\"
ip_only: ${ip_only}
use_cloudflare: ${use_cloudflare}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wizard-generated YAML interpolates user input directly into double-quoted scalars without escaping. Inputs containing ", backslashes, or newlines will produce invalid YAML (and could also change parsing semantics). Reuse the configurator’s YAML escaping approach (escape backslashes/quotes/newlines) or avoid quoting by emitting YAML-safe scalars.

Copilot uses AI. Check for mistakes.


def validate_api_key_optional(value: str) -> str | bool:
"""Accept empty or any non-whitespace string."""
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_api_key_optional() claims to accept “empty or any non-whitespace string”, but it currently returns True for whitespace-only inputs as well. Either update the docstring to match behavior or implement the intended value.strip() check so whitespace-only isn’t treated as a valid key.

Suggested change
"""Accept empty or any non-whitespace string."""
"""Accept empty or any non-whitespace string."""
if value == "":
return True
if not value.strip():
return "API key cannot be whitespace-only."

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Smart online installer — auto-detect RVSconfig.yml, interactive URL/paste/wizard prompt

2 participants