Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/confluent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ var (
)

func main() {
// Must precede both the config load and command construction, since each resolves paths that
// depend on the channel.
pversion.SetProcessChannel(pversion.ChannelOf(version))

cfg := config.New()

err := cfg.Load()
Expand Down
5 changes: 5 additions & 0 deletions cmd/docs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import (
// This code is adapted from https://github.com/spf13/cobra/blob/master/doc/rest_docs.md

func main() {
// Docs describe the released CLI, so paths in them must be the stable channel's; this binary
// carries no version stamp and would otherwise resolve to a dev build. Set before anything
// else, since the channel is an unsynchronized global and the test server below is concurrent.
pversion.SetProcessChannel(pversion.Stable)

// Set up test server for feature flags called by the code
testBackend := testserver.StartTestCloudServer(&testing.T{}, true)
defer testBackend.Close()
Expand Down
4 changes: 4 additions & 0 deletions cmd/lint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@ func init() {
}

func main() {
// Lint the released CLI's surface, so paths baked into flag defaults are the stable channel's.
// Must precede command construction; this binary carries no version stamp of its own.
pversion.SetProcessChannel(pversion.Stable)

// Set up test server for feature flags called by the code
testBackend := testserver.StartTestCloudServer(&testing.T{}, true)
defer testBackend.Close()
Expand Down
4 changes: 4 additions & 0 deletions cmd/whitelist/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ var version = "v0.0.0"
*/

func main() {
// Whitelist the released CLI's surface, so paths baked into flag defaults are the stable
// channel's. Must precede command construction; this binary carries no version stamp.
pversion.SetProcessChannel(pversion.Stable)

fmt.Println("BEGIN;")
fmt.Println("")
fmt.Println("INSERT INTO whitelist(version, keyword) VALUES")
Expand Down
2 changes: 1 addition & 1 deletion internal/configuration/command_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
func (c *command) newListCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List user-configurable fields in ~/.confluent/config.json.",
Short: "List user-configurable fields in the CLI configuration file.",
Args: cobra.NoArgs,
RunE: c.list,
}
Expand Down
5 changes: 3 additions & 2 deletions internal/plugin/command_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/confluentinc/cli/v4/pkg/config"
"github.com/confluentinc/cli/v4/pkg/errors"
"github.com/confluentinc/cli/v4/pkg/output"
"github.com/confluentinc/cli/v4/pkg/plugin"
Expand All @@ -27,11 +28,11 @@ func (c *command) newInstallCommand() *cobra.Command {
}

func (c *command) install(_ *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
confluentDir, err := config.StateDir()
if err != nil {
return err
}
confluentDir := filepath.Join(home, ".confluent")

dir, err := os.MkdirTemp(confluentDir, "cli-plugins")
if err != nil {
return err
Expand Down
5 changes: 3 additions & 2 deletions internal/plugin/command_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"gopkg.in/yaml.v3"

pcmd "github.com/confluentinc/cli/v4/pkg/cmd"
"github.com/confluentinc/cli/v4/pkg/config"
"github.com/confluentinc/cli/v4/pkg/output"
"github.com/confluentinc/cli/v4/pkg/plugin"
"github.com/confluentinc/cli/v4/pkg/utils"
Expand Down Expand Up @@ -50,12 +51,12 @@ func (c *command) newSearchCommand() *cobra.Command {
}

func (c *command) search(cmd *cobra.Command, _ []string) error {
home, err := os.UserHomeDir()
stateDir, err := config.StateDir()
if err != nil {
return err
}

dir, err := os.MkdirTemp(filepath.Join(home, ".confluent"), "cli-plugins")
dir, err := os.MkdirTemp(stateDir, "cli-plugins")
if err != nil {
return err
}
Expand Down
23 changes: 22 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,9 +648,30 @@ func (c *Config) GetFilename() string {
return c.Filename
}

// StateDirName is the name of the CLI's state directory within the user's home directory. It is
// scoped to the build's release channel so a production install, a prerelease, and a local build
// cannot read or overwrite each other's state. A stable build returns ".confluent", unchanged.
func StateDirName() string {
return ".confluent" + pversion.ProcessChannel().StateDirSuffix()
}

// StateDir is the absolute path of the CLI's state directory.
func StateDir() (string, error) {
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.",
)
Comment on lines +660 to +665

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added on this branch: TestStateDir_ErrorWhenHomeUnresolvable in pkg/config/config_test.go clears HOME/USERPROFILE and asserts StateDir returns an error rather than falling back to the working directory.

}
return filepath.Join(home, StateDirName()), nil
}

// GetDefaultFilename swallows a missing home directory because it backs a flag default built at
// command-construction time, where there is no error to return. Prefer StateDir where you can.
func GetDefaultFilename() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".confluent", "config.json")
return filepath.Join(home, StateDirName(), "config.json")
}

func (c *Config) CheckIsOnPremLogin() error {
Expand Down
79 changes: 78 additions & 1 deletion pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,10 +565,87 @@ func TestConfig_OverwrittenEnvironment(t *testing.T) {
func TestConfig_getFilename(t *testing.T) {
home, err := os.UserHomeDir()
require.NoError(t, err)
path := filepath.Join(home, ".confluent", "config.json")

// Set explicitly rather than relying on the package default, so this cannot start passing or
// failing because of a channel another test left behind.
t.Cleanup(func() { pversion.SetProcessChannel(pversion.Dev) })
pversion.SetProcessChannel(pversion.Dev)

path := filepath.Join(home, ".confluent-dev", "config.json")
require.Equal(t, path, New().GetFilename())
}

func TestConfig_getFilename_perChannel(t *testing.T) {
home, err := os.UserHomeDir()
require.NoError(t, err)
t.Cleanup(func() { pversion.SetProcessChannel(pversion.Dev) })

// Driven by the version strings the build system actually produces, so this fails if either the
// classifier or the directory naming drifts.
tests := []struct {
name string
version string
dir string
}{
{"GA release", "4.72.0", ".confluent"},
{"release candidate", "5.0.0-rc1", ".confluent-prerelease"},
{"local goreleaser snapshot", "4.72.0-SNAPSHOT-d962911bb", ".confluent-dev"},
{"nothing stamped", "0.0.0", ".confluent-dev"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pversion.SetProcessChannel(pversion.ChannelOf(test.version))

require.Equal(t, filepath.Join(home, test.dir, "config.json"), New().GetFilename())
})
}
}

func TestStateDir_ErrorWhenHomeUnresolvable(t *testing.T) {
// Clearing the home-directory env vars is what makes os.UserHomeDir fail: HOME on Unix,
// USERPROFILE on Windows. Both are cleared so the test is platform-agnostic.
t.Setenv("HOME", "")
t.Setenv("USERPROFILE", "")
if runtime.GOOS == "windows" {
// os.UserHomeDir consults HOMEDRIVE+HOMEPATH before erroring on Windows.
t.Setenv("HOMEDRIVE", "")
t.Setenv("HOMEPATH", "")
}

_, err := StateDir()

require.Error(t, err, "StateDir must fail rather than fall back to the working directory")
}

func TestStateDir_ByChannel(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Cleanup(func() { pversion.SetProcessChannel(pversion.Dev) })

tests := []struct {
name string
channel pversion.Channel
dir string
}{
{"stable keeps the historical path", pversion.Stable, ".confluent"},
{"prerelease is isolated", pversion.Prerelease, ".confluent-prerelease"},
{"dev is isolated", pversion.Dev, ".confluent-dev"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pversion.SetProcessChannel(test.channel)

dir, err := StateDir()

require.NoError(t, err)
require.Equal(t, filepath.Join(home, test.dir), dir)
})
}
}

func TestConfig_AddContext(t *testing.T) {
filename := "/tmp/TestConfig_AddContext.json"
conf := AuthenticatedOnPremConfigMock()
Expand Down
8 changes: 7 additions & 1 deletion pkg/flink/config/env_variables.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package config

// Keys/values for environment variables in pairs followed by default values
// 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"

// HomeConfluentPathDefault is the legacy stable-channel state directory name.
//
// Deprecated: the state directory now follows the build's release channel; use
// config.StateDirName instead. Retained as an exported alias for source compatibility.
const HomeConfluentPathDefault = ".confluent"
5 changes: 4 additions & 1 deletion pkg/flink/internal/history/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"

pconfig "github.com/confluentinc/cli/v4/pkg/config"
"github.com/confluentinc/cli/v4/pkg/flink/config"
"github.com/confluentinc/cli/v4/pkg/log"
)
Expand Down Expand Up @@ -58,9 +59,11 @@ func initPath(file string) *History {
return nil
}

// HOME_CONFLUENT_PATH predates channel-scoped state directories and still wins, so anyone
// relying on it keeps the layout they have.
confluentDir := os.Getenv(config.HomeConfluentPathEnvVar)
if confluentDir == "" {
confluentDir = config.HomeConfluentPathDefault
confluentDir = pconfig.StateDirName()
}
confluentPath := filepath.Join(home, confluentDir)
historyPath := filepath.Join(confluentPath, file)
Expand Down
47 changes: 47 additions & 0 deletions pkg/flink/internal/history/history_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import (
"testing"

"github.com/stretchr/testify/require"

pconfig "github.com/confluentinc/cli/v4/pkg/config"
"github.com/confluentinc/cli/v4/pkg/flink/config"
pversion "github.com/confluentinc/cli/v4/pkg/version"
)

func TestLoadHistory(t *testing.T) {
Expand Down Expand Up @@ -47,6 +51,49 @@ func TestLoadHistory(t *testing.T) {
history.historyPath = prevPath
}

func TestInitPath_ScopesByChannel(t *testing.T) {
home, err := os.UserHomeDir()
require.NoError(t, err)
t.Setenv(config.HomeConfluentPathEnvVar, "")
t.Cleanup(func() { pversion.SetProcessChannel(pversion.Dev) })

tests := []struct {
name string
channel pversion.Channel
dir string
}{
{"stable keeps the historical path", pversion.Stable, ".confluent"},
{"dev is isolated", pversion.Dev, ".confluent-dev"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pversion.SetProcessChannel(test.channel)

history := initPath(filename)

require.NotNil(t, history)
require.Equal(t, filepath.Join(home, test.dir), history.confluentPath)
})
}
}

func TestInitPath_HomeConfluentPathOverridesChannel(t *testing.T) {
home, err := os.UserHomeDir()
require.NoError(t, err)
t.Setenv(config.HomeConfluentPathEnvVar, ".confluent-custom")
t.Cleanup(func() { pversion.SetProcessChannel(pversion.Dev) })
pversion.SetProcessChannel(pversion.Stable)

history := initPath(filename)

require.NotNil(t, history)
require.Equal(t, filepath.Join(home, ".confluent-custom"), history.confluentPath,
"HOME_CONFLUENT_PATH must win over the channel default")
require.NotEqual(t, filepath.Join(home, pconfig.StateDirName()), history.confluentPath,
"the override must actually diverge from the channel default it is overriding")
}

func TestHistorySave(t *testing.T) {
// Create a temp directory
tmpDir, _ := os.MkdirTemp("", "confluent-test")
Expand Down
22 changes: 12 additions & 10 deletions pkg/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,19 @@ type pluginInfo struct {

// SearchPath goes through the files in the user's $PATH and checks if they are plugins
func SearchPath(cfg *config.Config) map[string][]string {
if runtime.GOOS == "windows" {
log.CliLogger.Debugf(`Searching $PATH and %%USERPROFILE%%\.confluent\plugins for plugins. Plugins can be disabled in %s.`, cfg.GetFilename())
} else {
log.CliLogger.Debugf("Searching $PATH and ~/.confluent/plugins for plugins. Plugins can be disabled in %s.", cfg.GetFilename())
}

pathDirList := filepath.SplitList(os.Getenv("PATH"))
home, _ := os.UserHomeDir()
pluginDir := filepath.Join(home, ".confluent", "plugins")
if !slices.Contains(pathDirList, pluginDir) {
pathDirList = append(pathDirList, pluginDir)

// Degrading to $PATH alone is deliberate: a relative plugin directory would pick up executables
// from whatever directory the CLI happens to be run in.
stateDir, err := config.StateDir()
if err != nil {
log.CliLogger.Debugf("Searching only $PATH for plugins: %v. Plugins can be disabled in %s.", err, cfg.GetFilename())
} else {
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)
}
}

plugins := make(map[string][]string)
Expand Down
Loading
Loading