Skip to content

Add credential profiles for working with multiple Fly.io accounts - #5087

Draft
mahdi-salmanzade wants to merge 1 commit into
superfly:masterfrom
mahdi-salmanzade:upstream-profiles
Draft

Add credential profiles for working with multiple Fly.io accounts#5087
mahdi-salmanzade wants to merge 1 commit into
superfly:masterfrom
mahdi-salmanzade:upstream-profiles

Conversation

@mahdi-salmanzade

Copy link
Copy Markdown

Change Summary

What and Why:

flyctl holds one login at a time. Working across several Fly.io accounts means fly auth logout / fly auth login on every context switch, and there is no way to ask which account a command is about to touch. This has come up a few times (#1641, #22869).

This adds credential profiles: several accounts on one machine, selected per shell, per project or per command.

fly profile add work
fly profile add client-a

cd ~/projects/acme
fly profile link client-a       # writes .fly-profile

fly deploy                      # uses client-a. no switching, no flags

How:

FLY_CONFIG_DIR already does the isolating, and it is already maintained here. What was missing is the layer that decides which directory to point at, so that is all this adds.

A profile is a complete config directory rather than just a token, so each one carries its own access token, metrics token, WireGuard peer state, fly-agent.sock and lock files. $HOME is untouched, so Docker and SSH credentials keep working.

The default profile is ~/.fly. An installation that never runs fly profile is unaffected — no migration, no new files.

Resolution, highest precedence first:

# Rule Scope
1 FLY_CONFIG_DIR pins a directory, bypassing profiles
2 --profile <name> one command
3 FLY_PROFILE=<name> one shell
4 .fly-profile, nearest at or above the cwd a directory tree
5 fly profile use <name> the machine
6 default ~/.fly

FLY_ACCESS_TOKEN / FLY_API_TOKEN still override the resolved config exactly as today, so existing CI is unaffected.

Two decisions I would most like review on:

  • A profile named by any rule that does not exist is a hard error, not a silent fallback. Reaching a different account than the one asked for seemed clearly worse than refusing to run.
  • That created a lockout, handled explicitly. A .fly-profile naming a deleted profile broke every command, including the ones needed to fix it. The fly profile commands now carry an annotation that lets them fall back to the default profile and report the cause, while fly deploy and friends still refuse. fly profile show diagnoses the dangling reference.

Two existing import cycles shaped the wiring. preparers cannot import internal/flag (flag → completion → preparers) or internal/command (command → preparers), so resolution hangs off the flagctx and command_context leaf packages already added for that reason; each gets a nil-safe accessor. flyctl.InitConfig() runs before cobra parses anything, which is why --profile is read from os.Args there — it is what keeps lock files and agent logs per-account, and it falls back silently since the preparer re-resolves and raises a much better error.

Files touched:

File Change
internal/profile/ new — store and resolver
internal/command/profile/ new — the fly profile command group
internal/cmdutil/preparers/preparers.go DetermineConfigDir consults the resolver
internal/command/root/root.go registers --profile and the command group
flyctl/flyctl.go pre-flag-parse config dir is profile-aware too
internal/flag/flagctx/helpers.go nil-safe flag set accessor
internal/command_context/context.go nil-safe command accessor + annotation lookup

Kept deliberately small and additive: 2 new packages, 5 touched files, ~1950 lines including tests and docs.

Testing:

  • 21 unit tests in internal/profile covering the precedence chain, directory walk-up, name validation against path traversal, dangling-reference errors, and the active-pointer lifecycle across remove/rename.
  • make test passes with zero failures across the whole suite; go vet ./... is clean.
  • Manual end-to-end: full precedence chain, nested-directory resolution, --refresh token verification, add rolling back cleanly and leaving no orphan directory when handed a bad token, and shell completion not panicking under an unresolvable profile.
  • Backward compatibility checked against a real account: with no profiles configured, fly auth whoami returns the same result as a stock v0.4.73 build against an untouched ~/.fly.

I have not written a preflight test — this is all local config resolution with no platform interaction, so unit tests seemed the right fit, but happy to add one if you would prefer.

Related to:

Opened as a draft per CONTRIBUTING's guidance for outside contributors. Happy to reshape any of this — naming, the hard-error decision, or where the docs live.


Documentation

  • Fresh Produce
  • In superfly/docs, or asked for help from docs team
  • n/a

This adds user-facing commands, so it needs docs I have not filed. profiles.md in this PR follows the existing building.md convention at the repo root, but if this belongs in superfly/docs instead, tell me where and I will move it.

flyctl holds one login at a time, so working across several accounts means
logging out and back in, and there is no way to tell which account a command is
about to touch.

FLY_CONFIG_DIR already provides the isolation; what is missing is the layer that
decides which config directory to point at. This adds that layer.

A profile is a whole config directory rather than just a token, so each carries
its own access token, WireGuard peer state and agent socket. The default profile
is ~/.fly itself, leaving existing installations unchanged.

Resolution runs flag > FLY_PROFILE > .fly-profile file > active pointer >
default, with FLY_CONFIG_DIR still pinning a directory outright. A named profile
that does not exist is an error rather than a silent fallback, since reaching a
different account than the one asked for is worse than not running. The profile
commands are exempt so a dangling reference stays repairable.

flyctl.InitConfig is resolved the same way so lock files and agent logs stay
per-account. It runs before cobra parses anything, hence reading --profile from
os.Args there.

Two existing import cycles shaped the wiring: preparers cannot import
internal/flag (flag -> completion -> preparers) or internal/command (command ->
preparers), so this uses the flagctx and command_context leaf packages, adding
nil-safe accessors to each.
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.

1 participant