fix(setup): refuse --dev rather than silently installing the release channel

`felis setup --dev` promised "install the dev channel (main HEAD)" and
installed release: the flag only exported FELIS_CHANNEL, a variable nothing in
the tree reads. deploy/bootstrap.sh reads FELIS_VERSION_BOOTSTRAP.

Renaming the variable would have been a worse bug than the dead one, because it
would look wired. setup runs bootstrap with FELIS_BOOTSTRAP_FROM_TUI=1, and on
that arm every reader of FELIS_VERSION_BOOTSTRAP is unreachable: the channel
case and its validation live in resolve_install_ref, which the TUI path skips
outright, and use_release_binary is only consulted by the elif that
`if bootstrap_from_tui` already short-circuited. setup re-images the host from
the felis binary it is itself running; there is no channel to pick.

So the flag refuses, exits 2 and names FELIS_VERSION_BOOTSTRAP=dev on the
installer, which is the mechanism that does work. Refusing beats defaulting:
the operator asked for dev, and release is the one answer they did not want.
The refusal precedes the root check, or an unprivileged operator gets told
about sudo instead of about the channel.

channelName had no other caller and goes with it. Nothing else referenced
--dev -- no doc, no script, no test -- so this removes a promise the tree only
ever made to itself.
This commit is contained in:
flyemoji committed 2026-07-20 19:53:32 +09:00
1 parent e5ea51c0db
commit 8675cda001
2 files changed
+39 -16

No files matched your search

+13 -16
View File
@@ -26,15 +26,6 @@ const hostBootstrapKubeconfigPath = "/etc/rancher/k3s/k3s.yaml"
var errHostBootstrapCancelled = errors.New("host bootstrap cancelled")
// channelName maps the --dev flag to the release channel deploy/bootstrap.sh
// understands. Release is the default so a bare `felis setup` is production.
func channelName(dev bool) string {
if dev {
return "dev"
}
return "release"
}
// cmdSetup is the normal first-run operator console. It is intentionally separate
// from breakGlass: setup creates the initial Owner and optional web edge; breakGlass
// is reserved for emergency local recovery/reset.
@@ -42,19 +33,25 @@ func cmdSetup(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("setup", flag.ContinueOnError)
fs.SetOutput(stderr)
cfgPath := fs.String("config", defaultSetupConfigPath, "path to felis.toml")
dev := fs.Bool("dev", false, "install the dev channel (felis:dev, main HEAD) instead of the default release channel (felis:release, newest tag)")
dev := fs.Bool("dev", false, "rejected: the install channel is chosen by the bootstrap installer, not by setup")
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
// The channel governs which image tag/source ref the host bootstrap builds.
// runBootstrap forwards the whole environment, so exporting it here is enough
// to reach deploy/bootstrap.sh without threading a parameter through the TUI.
if err := os.Setenv("FELIS_CHANNEL", channelName(*dev)); err != nil {
fmt.Fprintf(stderr, "felis setup: %v\n", err)
return 1
// setup cannot honour a channel, so it refuses rather than silently installing the
// other one. It used to export FELIS_CHANNEL here, which nothing has ever read --
// deploy/bootstrap.sh reads FELIS_VERSION_BOOTSTRAP -- so --dev was a silent no-op
// that installed release. Renaming the variable would not fix it: on this path
// bootstrap takes the bootstrap_from_tui arm, which re-images the host from the
// binary setup is already running, and every reader of FELIS_VERSION_BOOTSTRAP
// (use_release_binary, resolve_install_ref) is unreachable from there. Choosing a
// channel means re-running the installer, which is what this points the operator at.
if *dev {
fmt.Fprintln(stderr, "felis setup: --dev is not supported here; setup re-images this host from the felis binary it is already running.")
fmt.Fprintln(stderr, "To install a different channel, re-run the bootstrap installer with FELIS_VERSION_BOOTSTRAP=dev (see CONTRIBUTING.md).")
return 2
}
configFlagSet := false
fs.Visit(func(f *flag.Flag) {