feat(config): scope state directory by release channel - #3467
feat(config): scope state directory by release channel#3467Dave Shoup (shouples) wants to merge 2 commits into
Conversation
|
🎉 All Contributor License Agreements have been signed. Ready to merge. |
A dev or prerelease binary that shares ~/.confluent with a production install can read and overwrite its contexts and credentials. Classify the build's channel from the version stamped in at link time and derive the state directory from it: stable keeps ~/.confluent unchanged, a prerelease uses ~/.confluent-prerelease, and any local build uses ~/.confluent-dev. Config, the managed plugins directory, and Flink statement history all follow it. Stable-channel behavior is unchanged, so installed CLIs are unaffected. Touches pkg/version, pkg/config, and pkg/plugin (restricted zones) by necessity; the mechanism has nowhere else to live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1426133 to
d825ade
Compare
There was a problem hiding this comment.
Pull request overview
Scopes CLI state by release channel while preserving ~/.confluent for stable releases.
Changes:
- Adds stable, prerelease, and dev channel classification.
- Applies channel-specific paths to config, plugins, and Flink history.
- Adds tests, integration coverage, and updated help output.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Summary |
|---|---|
test/live/live_test.go |
Uses channel-aware test state paths. |
test/fixtures/output/configuration/list-help.golden |
Updates configuration help output. |
test/fixtures/output/configuration/list-help-onprem.golden |
Updates on-premises help output. |
test/fixtures/output/configuration/help.golden |
Updates configuration command help. |
test/fixtures/output/configuration/help-onprem.golden |
Updates on-premises configuration help. |
test/channel_state_test.go |
Adds stamped-build state isolation tests. |
pkg/version/channel.go |
Implements release-channel classification. |
pkg/version/channel_test.go |
Tests channel classification behavior. |
pkg/plugin/plugin.go |
Discovers plugins in channel-specific directories. |
pkg/flink/internal/history/history.go |
Scopes Flink history by channel. |
pkg/flink/config/env_variables.go |
Updates Flink path configuration. Moderate (2 votes): retain the deprecated exported HomeConfluentPathDefault alias for source compatibility. |
pkg/config/config.go |
Adds channel-aware state directory helpers. Nit (3 votes): add coverage for the StateDir error branch. |
pkg/config/config_test.go |
Tests channel-specific configuration paths. |
internal/plugin/command_search.go |
Uses the channel state directory for temporary files. |
internal/plugin/command_install.go |
Installs plugins under the channel state directory. |
internal/configuration/command_list.go |
Genericizes the hardcoded configuration path in help. |
cmd/whitelist/main.go |
Uses the stable channel for generated whitelist data. |
cmd/lint/main.go |
Uses the stable channel for linting. |
cmd/docs/main.go |
Uses the stable channel for documentation generation. |
cmd/confluent/main.go |
Initializes the channel before configuration loading. |
Suppressed comments (8)
pkg/config/config.go:674
Config.Loadstill obtains its default filename through this helper (Config.GetFilename), andGetDefaultFilenameignoresos.UserHomeDirerrors. WithHOMEunset, it returns.confluent-dev/config.jsonrelative to the working directory;Loadthen callsSave, which creates and writes that path. Thus normal CLI startup still silently stores state in the working directory despite the newStateDirerror path. Propagate the home-directory error through config initialization/loading instead of retaining this fallback.
func GetDefaultFilename() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, StateDirName(), "config.json")
pkg/flink/internal/history/history.go:67
- The history tests only assert that the paths are non-empty, so they do not verify this new channel-dependent default. Please add a focused test for
initPathunder at least the dev and stable channels (while preserving theHOME_CONFLUENT_PATHoverride) to catch Flink history being written to the wrong state directory.
confluentDir := os.Getenv(config.HomeConfluentPathEnvVar)
if confluentDir == "" {
confluentDir = pconfig.StateDirName()
}
pkg/plugin/plugin.go:42
- The existing
TestSearchPathonly verifies plugins found throughPATH; it never exercises the new channel-scopedstateDir/pluginsentry added here. A regression could make installed plugins undiscoverable while the suite remains green, so please add a test that creates a plugin underconfig.StateDir()/pluginsand verifies it is returned.
pluginDir := filepath.Join(stateDir, "plugins")
log.CliLogger.Debugf("Searching $PATH and %s for plugins. Plugins can be disabled in %s.", pluginDir, cfg.GetFilename())
if !slices.Contains(pathDirList, pluginDir) {
pathDirList = append(pathDirList, pluginDir)
}
pkg/version/channel.go:40
- The
defaultbranch maps every unknownChannelvalue to the stable suffix, soSetProcessChannel(Channel(99))(or a newly added enum value before this switch is updated) silently shares.confluentwith production. That violates the isolation rule and makes an unfamiliar value risk customer state; handleStableexplicitly and make the default fall back to the dev suffix (or reject invalid values).
default:
return ""
pkg/version/channel.go:68
strings.Containsclassifies any prerelease containing the substringsnapshotas a local build. A valid published label such as4.73.0-snapshot-reviewor4.73.0-not-snapshot.1would therefore use.confluent-devinstead of.confluent-prerelease, contrary to the classifier's documented rule. MatchSNAPSHOTas a delimiter-bounded token in the goreleaser form instead.
case strings.Contains(strings.ToUpper(prerelease), snapshotMarker):
test/channel_state_test.go:64
- On Windows this
go build -otarget has no.exesuffix. The repository's Windows integration target explicitly buildstest/bin/confluent.exe(Makefile:109), soexec.Command(binary, ...)can fail to locate the generated PE binary here. Give the temporary output a platform-appropriate executable suffix.
binary := filepath.Join(t.TempDir(), "confluent")
test/channel_state_test.go:69
- The stamped binaries leave
main.isTestfalse. Both the defaultversioninvocation andconfiguration updaterunnotifyIfUpdateAvailable(pkg/cmd/prerunner.go:109), so each test can call the real update service and mutateLastUpdateCheckAtin the temporary config. Build these fixtures with-X main.isTest=true(as the normal integration build does) to keep this test hermetic.
args := []string{"build", "-o", binary}
if version != "" {
args = append(args, "-ldflags=-X main.version="+version)
}
args = append(args, "../cmd/confluent")
test/channel_state_test.go:69
- This helper assumes the test process's current directory is
test, butTestCLI.SetupSuitechanges the shared process directory to the repository root attest/cli_test.go:85and does not restore it. If this top-level test runs afterTestCLI,../cmd/confluentresolves outside the checkout and the build fails; derive the repository root independently of the current working directory or restore the directory after the suite.
args = append(args, "../cmd/confluent")
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| home, err := os.UserHomeDir() | ||
| if err != nil { | ||
| return "", errors.NewErrorWithSuggestions( | ||
| fmt.Sprintf("unable to determine the home directory holding the CLI's state: %v", err), | ||
| "Set the `HOME` environment variable (`USERPROFILE` on Windows) to a writable directory.", | ||
| ) |
| // Overrides the directory under $HOME that Flink statement history is written to. The default is | ||
| // no longer a constant here: it follows the build's release channel, via config.StateDirName. | ||
| const HomeConfluentPathEnvVar = "HOME_CONFLUENT_PATH" |
d825ade to
8fead41
Compare
8fead41 to
2e8e322
Compare
|


Release Notes
No user-facing changes for published releases. A GA (stable) build keeps its state in
~/.confluentexactly as before; only locally-built binaries move to their own directory today (with a prerelease directory reserved for when the pipeline can produce one - seeWhat).Checklist
Whatsection below whether this PR applies to Confluent Cloud, Confluent Platform, or both.Test & Reviewsection below.Blast Radiussection below.Applies to both Cloud and Platform. Feature-flag items are N/A - this is not a gated feature, and a stable build's behavior is unchanged.
What
A locally-built or prerelease CLI shares its state directory (
~/.confluent) with an installed GA release, so logins, contexts, and cached tokens overwrite each other. Testing a dev build could log you out of your real CLI, and vice versa. This PR derives the state directory from the build's release channel so the three kinds of binary self-isolate.The channel is derived automatically from the version string the linker stamps into the binary - not a flag, environment variable, or
maketarget anyone sets.main()classifies it viaChannelOfbefore the config load or command construction, since both resolve the state-dir path:4.73.0->~/.confluent(unchanged, so existing installs are unaffected)4.73.0-rc1->~/.confluent-prerelease0.0.0binary, any0.xversion, or amake buildsnapshot (...-SNAPSHOT-<sha>) ->~/.confluent-devThe prerelease channel isn't reachable yet - no
-rc/preview tag exists in this repo, and the release pipeline doesn't emit one today - but the classifier is ready for when it does. Why prerelease needs its own bucket instead of folding into dev, and the RC-cycle edge case that forces it, is in the collapsed section below.Along the way,
config.StateDir()now returns an error instead of silently writing state into the working directory when the home directory can't be resolved, and one of three hardcoded~/.confluent/config.jsonhelp strings is genericized (the other two are deferred cleanup).Applies to: both Confluent Cloud and Confluent Platform.
Why three channels instead of just prod / non-prod?
They map to three audiences whose state must not mix: production users, testers validating a published candidate, and the developer building locally. Folding prerelease into dev breaks during an RC cycle:
make buildstamps aSNAPSHOTsuffix onto the RC's own version (e.g.4.73.0-rc1-SNAPSHOT-<sha>), so without a separate dev bucket that local build would land in the same directory as the published candidate a tester relies on.channel_test.gocovers this case directly.Blast Radius
The risk concentrates in the channel classifier: if it misclassified a stable release as prerelease or dev, that release would look for state in the wrong directory and behave as if freshly installed - customers would appear logged out and lose their configured contexts until they re-ran
confluent login. This is why stable maps to the historical path with an empty suffix and is covered bypkg/version/channel_test.go, and why the classifier errs toward isolating an unfamiliar build rather than assuming stable. No data is deleted; the old directory is left intact.References
fix-log-flush; the base of the channel-state / dev-build work.Test & Review
pkg/version/channel_test.gocovers the classifier across stable, prerelease, snapshot/dev, and malformed version strings (including the-rc1-SNAPSHOTcase a release-candidatemake buildproduces).pkg/config/config_test.gocoversStateDir()/StateDirName(), including the stable path staying.confluentand the error path when the home directory is unresolvable.test/channel_state_test.gois a new integration test asserting the running binary resolves its state directory by channel.make build(a dev binary) writes to~/.confluent-dev, leaving an installed release's~/.confluentuntouched.go build ./...andgo test ./pkg/version/... ./pkg/config/...pass.