From b0cd1a9274570d4923f0a53c780d4165f248a402 Mon Sep 17 00:00:00 2001 From: Marc Campbell Date: Wed, 5 Nov 2025 07:50:07 -0600 Subject: [PATCH] Make cache profile-aware --- cli/cmd/logout.go | 10 +++++ cli/cmd/profile_add.go | 25 ++++++------- cli/cmd/profile_rm.go | 8 ++++ cli/cmd/root.go | 47 ++++++++++++++--------- cli/cmd/runner.go | 2 +- pkg/cache/cache.go | 84 ++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 140 insertions(+), 36 deletions(-) diff --git a/cli/cmd/logout.go b/cli/cmd/logout.go index 4ee6ad786..71d91d486 100644 --- a/cli/cmd/logout.go +++ b/cli/cmd/logout.go @@ -1,7 +1,11 @@ package cmd import ( + "fmt" + "os" + "github.com/pkg/errors" + replicatedcache "github.com/replicatedhq/replicated/pkg/cache" "github.com/replicatedhq/replicated/pkg/credentials" "github.com/spf13/cobra" ) @@ -34,5 +38,11 @@ func (r *runners) logout(_ *cobra.Command, _ []string) error { return err } + // Clear all cache files on logout + if err = replicatedcache.DeleteAllCacheFiles(); err != nil { + // Don't fail logout if cache clear fails, just warn + fmt.Fprintf(os.Stderr, "Warning: failed to clear cache: %v\n", err) + } + return nil } diff --git a/cli/cmd/profile_add.go b/cli/cmd/profile_add.go index 9bd3f9b3e..706914bb3 100644 --- a/cli/cmd/profile_add.go +++ b/cli/cmd/profile_add.go @@ -21,17 +21,11 @@ Optionally, you can specify custom API and registry origins. If a profile with the same name already exists, it will be updated. The profile will be stored in ~/.replicated/config.yaml with file permissions 600 (owner read/write only).`, - Example: `# Add a production profile (will prompt for token) -replicated profile add prod + Example: `# Add a team profile (will prompt for token) +replicated profile add my-team -# Add a production profile with token flag -replicated profile add prod --token=your-prod-token - -# Add a development profile with custom origins -replicated profile add dev \ - --token=your-dev-token \ - --api-origin=https://vendor-api-noahecampbell.okteto.repldev.com \ - --registry-origin=vendor-registry-v2-noahecampbell.okteto.repldev.com`, +# Add a team profile with token flag +replicated profile add my-team --token=your-token`, Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: r.profileAdd, @@ -39,9 +33,12 @@ replicated profile add dev \ parent.AddCommand(cmd) cmd.Flags().StringVar(&r.args.profileAddToken, "token", "", "API token for this profile (optional, will prompt if not provided)") - cmd.Flags().StringVar(&r.args.profileAddAPIOrigin, "api-origin", "", "API origin (optional, e.g., https://api.replicated.com/vendor). Mutually exclusive with --namespace") - cmd.Flags().StringVar(&r.args.profileAddRegistryOrigin, "registry-origin", "", "Registry origin (optional, e.g., registry.replicated.com). Mutually exclusive with --namespace") - cmd.Flags().StringVar(&r.args.profileAddNamespace, "namespace", "", "Okteto namespace for dev environments (e.g., 'noahecampbell'). Auto-generates service URLs. Mutually exclusive with --api-origin and --registry-origin") + cmd.Flags().StringVar(&r.args.profileAddAPIOrigin, "api-origin", "", "API origin (optional, e.g., https://api.replicated.com/vendor).") + cmd.Flags().StringVar(&r.args.profileAddRegistryOrigin, "registry-origin", "", "Registry origin (optional, e.g., registry.replicated.com).") + cmd.Flags().StringVar(&r.args.profileAddOktetoNamespace, "okteto-namespace", "", "Okteto namespace for dev environments (e.g., 'your-namespace'). Auto-generates service URLs. Mutually exclusive with --api-origin and --registry-origin") + + // "okteto-namespace" is hidden, it's just for replicated users + cmd.Flags().MarkHidden("okteto-namespace") return cmd } @@ -76,7 +73,7 @@ func (r *runners) profileAdd(cmd *cobra.Command, args []string) error { APIToken: token, APIOrigin: r.args.profileAddAPIOrigin, RegistryOrigin: r.args.profileAddRegistryOrigin, - Namespace: r.args.profileAddNamespace, + Namespace: r.args.profileAddOktetoNamespace, } if err := credentials.AddProfile(profileName, profile); err != nil { diff --git a/cli/cmd/profile_rm.go b/cli/cmd/profile_rm.go index c3e3ecb00..2f6df46cc 100644 --- a/cli/cmd/profile_rm.go +++ b/cli/cmd/profile_rm.go @@ -2,8 +2,10 @@ package cmd import ( "fmt" + "os" "github.com/pkg/errors" + replicatedcache "github.com/replicatedhq/replicated/pkg/cache" "github.com/replicatedhq/replicated/pkg/credentials" "github.com/spf13/cobra" ) @@ -43,6 +45,12 @@ func (r *runners) profileRm(_ *cobra.Command, args []string) error { return errors.Wrap(err, "failed to get profile") } + // Remove the profile's cache file before removing the profile + if err := replicatedcache.DeleteCacheFile(profileName); err != nil { + // Don't fail profile removal if cache delete fails, just warn + fmt.Fprintf(os.Stderr, "Warning: failed to delete cache for profile '%s': %v\n", profileName, err) + } + // Remove the profile if err := credentials.RemoveProfile(profileName); err != nil { return errors.Wrap(err, "failed to remove profile") diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 9fe84457a..e29f754a1 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -44,11 +44,8 @@ func init() { platformOrigin = originFromEnv } - c, err := replicatedcache.InitCache() - if err != nil { - panic(err) - } - cache = c + // Note: Cache initialization is now deferred until we know which profile to use + // See initCacheForProfile() in preRunSetupAPIs // Set debug mode from environment variable if os.Getenv("REPLICATED_DEBUG") == "1" || os.Getenv("REPLICATED_DEBUG") == "true" { @@ -57,6 +54,16 @@ func init() { } } +// initCacheForProfile initializes the cache for the given profile name +func initCacheForProfile(profileName string) error { + c, err := replicatedcache.InitCache(profileName) + if err != nil { + return errors.Wrap(err, "failed to initialize cache") + } + cache = c + return nil +} + // RootCmd represents the base command when called without any subcommands func GetRootCmd() *cobra.Command { rootCmd := &cobra.Command{ @@ -312,19 +319,25 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i runCmds.rootCmd.SetUsageTemplate(rootCmdUsageTmpl) preRunSetupAPIs := func(cmd *cobra.Command, args []string) error { - if apiToken == "" { - // Try to load profile from --profile flag, then default profile - var profileName string - if profileNameFlag != "" { - // Command-line flag takes precedence - profileName = profileNameFlag - } else { - // Fall back to default profile from ~/.replicated/config.yaml - defaultProfileName, err := credentials.GetDefaultProfile() - if err == nil && defaultProfileName != "" { - profileName = defaultProfileName - } + // Determine profile name first + var profileName string + if profileNameFlag != "" { + // Command-line flag takes precedence + profileName = profileNameFlag + } else { + // Fall back to default profile from ~/.replicated/config.yaml + defaultProfileName, err := credentials.GetDefaultProfile() + if err == nil && defaultProfileName != "" { + profileName = defaultProfileName } + } + + // Initialize cache for this profile + if err := initCacheForProfile(profileName); err != nil { + return errors.Wrap(err, "initialize cache") + } + + if apiToken == "" { // Get credentials with profile support creds, err := credentials.GetCredentialsWithProfile(profileName) if err != nil { diff --git a/cli/cmd/runner.go b/cli/cmd/runner.go index a3847929a..f5e41ac18 100644 --- a/cli/cmd/runner.go +++ b/cli/cmd/runner.go @@ -282,7 +282,7 @@ type runnerArgs struct { profileAddToken string profileAddAPIOrigin string profileAddRegistryOrigin string - profileAddNamespace string + profileAddOktetoNamespace string profileEditToken string profileEditAPIOrigin string profileEditRegistryOrigin string diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 2f81aa175..5981b51cf 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -10,22 +10,31 @@ import ( "github.com/replicatedhq/replicated/pkg/types" ) -const cacheFileName = "replicated.cache" +const ( + legacyCacheFileName = "replicated.cache" + cacheFileNameFormat = "replicated-%s.cache" // profile-specific cache +) type Cache struct { DefaultApp string `json:"default_app"` LastAppRefresh *time.Time `json:"last_app_refresh"` Apps []types.App `json:"apps"` + + // profileName is the profile this cache is associated with + // Empty string means legacy/no profile + profileName string } -func InitCache() (*Cache, error) { +// InitCache creates or loads a cache for the given profile name. +// If profileName is empty, uses legacy cache file. +func InitCache(profileName string) (*Cache, error) { cacheDir, err := getCacheDir() if err != nil { return nil, errors.Wrap(err, "failed to get cache directory") } - cacheFilePath := filepath.Join(cacheDir, cacheFileName) + cacheFilePath := getCacheFilePath(cacheDir, profileName) // Try to load existing cache cache, err := loadCache(cacheFilePath) @@ -36,6 +45,7 @@ func InitCache() (*Cache, error) { Apps: []types.App{}, LastAppRefresh: nil, DefaultApp: "", + profileName: profileName, } // save it @@ -46,18 +56,29 @@ func InitCache() (*Cache, error) { } else { return nil, errors.Wrap(err, "failed to load cache") } + } else { + // Set the profile name on loaded cache + cache.profileName = profileName } return cache, nil } +// getCacheFilePath returns the cache file path for the given profile +func getCacheFilePath(cacheDir, profileName string) string { + if profileName == "" { + return filepath.Join(cacheDir, legacyCacheFileName) + } + return filepath.Join(cacheDir, filepath.Clean(profileName)+".cache") +} + func (c *Cache) Save() error { cacheDir, err := getCacheDir() if err != nil { return errors.Wrap(err, "failed to get cache directory") } - cacheFilePath := filepath.Join(cacheDir, cacheFileName) + cacheFilePath := getCacheFilePath(cacheDir, c.profileName) data, err := json.Marshal(c) if err != nil { @@ -138,3 +159,58 @@ func (c *Cache) ClearDefault(defaultType string) error { return nil } + +// ClearAll removes all cached data from the current cache +func (c *Cache) ClearAll() error { + c.Apps = []types.App{} + c.DefaultApp = "" + c.LastAppRefresh = nil + return c.Save() +} + +// DeleteCacheFile deletes the cache file for a specific profile +func DeleteCacheFile(profileName string) error { + cacheDir, err := getCacheDir() + if err != nil { + return errors.Wrap(err, "failed to get cache directory") + } + + cacheFilePath := getCacheFilePath(cacheDir, profileName) + + // It's not an error if the file doesn't exist + if err := os.Remove(cacheFilePath); err != nil && !os.IsNotExist(err) { + return errors.Wrap(err, "failed to delete cache file") + } + + return nil +} + +// DeleteAllCacheFiles removes all cache files (for logout) +func DeleteAllCacheFiles() error { + cacheDir, err := getCacheDir() + if err != nil { + return errors.Wrap(err, "failed to get cache directory") + } + + // Read all files in cache directory + files, err := os.ReadDir(cacheDir) + if err != nil { + if os.IsNotExist(err) { + return nil // Cache directory doesn't exist, nothing to delete + } + return errors.Wrap(err, "failed to read cache directory") + } + + // Delete all .cache files + for _, file := range files { + if !file.IsDir() && filepath.Ext(file.Name()) == ".cache" { + filePath := filepath.Join(cacheDir, file.Name()) + if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) { + // Log but don't fail on individual file deletion errors + continue + } + } + } + + return nil +}