diff --git a/cli/cmd/cache_helper.go b/cli/cmd/cache_helper.go index efcabd393..ce8a1deb3 100644 --- a/cli/cmd/cache_helper.go +++ b/cli/cmd/cache_helper.go @@ -4,11 +4,17 @@ import ( "context" "github.com/pkg/errors" + replicatedcache "github.com/replicatedhq/replicated/pkg/cache" "github.com/replicatedhq/replicated/pkg/kotsclient" "github.com/replicatedhq/replicated/pkg/types" ) func getApp(appSlugOrID string, kotsClient *kotsclient.VendorV3Client) (*types.App, error) { + cache, err := replicatedcache.GetInstance() + if err != nil { + return nil, errors.Wrap(err, "initialize cache") + } + app, err := cache.GetApp(appSlugOrID) if err == nil && app != nil { return app, nil diff --git a/cli/cmd/completion_test.go b/cli/cmd/completion_test.go index 31587a020..9e73693c3 100644 --- a/cli/cmd/completion_test.go +++ b/cli/cmd/completion_test.go @@ -2,11 +2,46 @@ package cmd import ( "bytes" + "os" "testing" + replicatedcache "github.com/replicatedhq/replicated/pkg/cache" "github.com/spf13/cobra" ) +// TestShellCompletionsNoHome verifies that completion commands work even when +// HOME is unset or invalid (e.g., during Nix builds). This is a regression +// test for https://github.com/replicatedhq/replicated/issues/535. +func TestShellCompletionsNoHome(t *testing.T) { + // Save and clear HOME to simulate an environment where cache initialization + // would fail. + origHome := os.Getenv("HOME") + os.Unsetenv("HOME") + os.Unsetenv("XDG_CACHE_HOME") + defer func() { + os.Setenv("HOME", origHome) + }() + + // Reset the lazy cache state so this test doesn't interfere with others. + replicatedcache.ResetForTesting() + defer replicatedcache.ResetForTesting() + + parentCmd := &cobra.Command{ + Use: "replicated", + } + out := bytes.NewBufferString("") + cmd := NewCmdCompletion(out, parentCmd.Name()) + parentCmd.AddCommand(cmd) + + err := RunCompletion(out, cmd, []string{"bash"}) + if err != nil { + t.Fatalf("Unexpected error with no HOME: %v", err) + } + if out.Len() == 0 { + t.Fatalf("Output was not written") + } +} + // This unit-test was adapted from kubectl repo // Link: https://github.com/kubernetes/kubectl/blob/826006cdb947f80a679ff1eb3cb53f183a6a9bf2/pkg/cmd/completion/completion_test.go func TestShellCompletions(t *testing.T) { diff --git a/cli/cmd/default_clear.go b/cli/cmd/default_clear.go index 46aae45b3..0ff9ccec7 100644 --- a/cli/cmd/default_clear.go +++ b/cli/cmd/default_clear.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/pkg/errors" + replicatedcache "github.com/replicatedhq/replicated/pkg/cache" "github.com/spf13/cobra" ) @@ -29,6 +30,11 @@ replicated default clear app`, } func (r *runners) clearDefault(cmd *cobra.Command, defaultType string) error { + cache, err := replicatedcache.GetInstance() + if err != nil { + return errors.Wrap(err, "initialize cache") + } + if err := cache.ClearDefault(defaultType); err != nil { return errors.Wrap(err, "clear default") } diff --git a/cli/cmd/default_clearall.go b/cli/cmd/default_clearall.go index 2a979fa14..db8e7b397 100644 --- a/cli/cmd/default_clearall.go +++ b/cli/cmd/default_clearall.go @@ -1,6 +1,10 @@ package cmd -import "github.com/spf13/cobra" +import ( + "github.com/pkg/errors" + replicatedcache "github.com/replicatedhq/replicated/pkg/cache" + "github.com/spf13/cobra" +) func (r *runners) InitDefaultClearAllCommand(parent *cobra.Command) *cobra.Command { cmd := &cobra.Command{ @@ -22,6 +26,11 @@ replicated default clear-all`, } func (r *runners) clearAllDefaults(cmd *cobra.Command) error { + cache, err := replicatedcache.GetInstance() + if err != nil { + return errors.Wrap(err, "initialize cache") + } + if err := cache.ClearDefault("app"); err != nil { return err } diff --git a/cli/cmd/default_set.go b/cli/cmd/default_set.go index db9ffee6c..804c02035 100644 --- a/cli/cmd/default_set.go +++ b/cli/cmd/default_set.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/pkg/errors" "github.com/replicatedhq/replicated/cli/print" + replicatedcache "github.com/replicatedhq/replicated/pkg/cache" "github.com/replicatedhq/replicated/pkg/types" "github.com/spf13/cobra" ) @@ -44,6 +45,11 @@ func (r *runners) setDefault(cmd *cobra.Command, defaultType string, defaultValu return errors.Wrap(err, "get app") } + cache, err := replicatedcache.GetInstance() + if err != nil { + return errors.Wrap(err, "initialize cache") + } + if err := cache.SetDefault(defaultType, defaultValue); err != nil { return errors.Wrap(err, "set default in cache") } diff --git a/cli/cmd/default_show.go b/cli/cmd/default_show.go index 5bcc9a110..347060ff2 100644 --- a/cli/cmd/default_show.go +++ b/cli/cmd/default_show.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/replicated/cli/print" + replicatedcache "github.com/replicatedhq/replicated/pkg/cache" "github.com/replicatedhq/replicated/pkg/types" "github.com/spf13/cobra" ) @@ -40,6 +41,11 @@ replicated default show app } func (r *runners) showDefault(cmd *cobra.Command, defaultType string, outputFormat string) error { + cache, err := replicatedcache.GetInstance() + if err != nil { + return errors.Wrap(err, "initialize cache") + } + defaultValue, err := cache.GetDefault(defaultType) if err != nil { return errors.Wrap(err, "get default value") diff --git a/cli/cmd/enterprise_portal_preview.go b/cli/cmd/enterprise_portal_preview.go index 73a299a5d..eaf9033e2 100644 --- a/cli/cmd/enterprise_portal_preview.go +++ b/cli/cmd/enterprise_portal_preview.go @@ -15,6 +15,7 @@ import ( "time" "github.com/pkg/errors" + replicatedcache "github.com/replicatedhq/replicated/pkg/cache" "github.com/replicatedhq/replicated/pkg/credentials" "github.com/replicatedhq/replicated/pkg/kotsclient" "github.com/replicatedhq/replicated/pkg/platformclient" @@ -106,8 +107,11 @@ func (r *runners) enterprisePortalPreview(cmd *cobra.Command, args []string) err // 2. default app set via `replicated default app ` (cache.DefaultApp) // 3. REPLICATED_APP env var appSlug := appSlugOrID - if appSlug == "" && cache != nil { - appSlug = cache.DefaultApp + if appSlug == "" { + cache, err := replicatedcache.GetInstance() + if err == nil && cache != nil { + appSlug = cache.DefaultApp + } } if appSlug == "" { appSlug = os.Getenv("REPLICATED_APP") diff --git a/cli/cmd/root.go b/cli/cmd/root.go index b2268218e..196c497ff 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -35,7 +35,6 @@ var ( profileNameFlag string platformOrigin = "https://api.replicated.com/vendor" kurlDotSHOrigin = "https://kurl.sh" - cache *replicatedcache.Cache debugFlag bool ) @@ -45,12 +44,6 @@ func init() { platformOrigin = originFromEnv } - c, err := replicatedcache.InitCache() - if err != nil { - panic(err) - } - cache = c - // Set debug mode from environment variable if os.Getenv("REPLICATED_DEBUG") == "1" || os.Getenv("REPLICATED_DEBUG") == "true" { debugFlag = true @@ -446,6 +439,11 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i return errors.Wrap(err, "set up APIs") } + cache, err := replicatedcache.GetInstance() + if err != nil { + return errors.Wrap(err, "initialize cache") + } + if appSlugOrID == "" { if cache.DefaultApp != "" { appSlugOrID = cache.DefaultApp diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index e53de1b69..01dc441cc 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "path/filepath" + "sync" "time" "github.com/adrg/xdg" @@ -11,6 +12,32 @@ import ( "github.com/replicatedhq/replicated/pkg/types" ) +var ( + instance *Cache + instanceErr error + once sync.Once +) + +// GetInstance returns the singleton cache instance, initializing it on first +// call. Lazy initialization ensures commands that don't need the cache (e.g. +// 'completion') won't fail when HOME is not writable. +func GetInstance() (*Cache, error) { + once.Do(func() { + instance, instanceErr = InitCache() + }) + if instanceErr != nil { + return nil, instanceErr + } + return instance, nil +} + +// ResetForTesting resets the singleton so tests can control cache state. +func ResetForTesting() { + once = sync.Once{} + instance = nil + instanceErr = nil +} + const cacheFileName = "replicated.cache" type Cache struct {