From 1b7fd03a3e85a204f0e2a3e342c8d875df32d4e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Bustarret?= Date: Thu, 23 Jul 2026 08:59:23 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(preferences):=20profile-based?= =?UTF-8?q?=20preferences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/kube.go | 25 ++++++++++---- cmd/kube_kubeapi.go | 3 +- cmd/kube_kubectl.go | 3 +- cmd/kube_test.go | 22 ++++++++++++ cmd/prerun/preferences.go | 36 +++++++++++++++++++ cmd/profile_test.go | 4 +++ cmd/root.go | 1 + pkg/preferences/preferences.go | 63 ++++++++++++++++++++++++++++------ 8 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 cmd/prerun/preferences.go diff --git a/cmd/kube.go b/cmd/kube.go index 1f3646f8..40eca3cb 100644 --- a/cmd/kube.go +++ b/cmd/kube.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/google/uuid" + "github.com/outscale/octl/cmd/prerun" "github.com/outscale/octl/pkg/builder" "github.com/outscale/octl/pkg/config" "github.com/outscale/octl/pkg/debug" @@ -20,6 +21,7 @@ import ( "github.com/outscale/octl/pkg/preferences" "github.com/outscale/octl/pkg/runner" "github.com/outscale/osc-sdk-go/v3/pkg/oks" + "github.com/outscale/osc-sdk-go/v3/pkg/profile" "github.com/samber/lo" "github.com/spf13/cobra" ) @@ -51,11 +53,11 @@ func init() { // Add --project flag to kube cluster commands clusterCmd, _ := lo.Find(oksCmd.Commands(), func(c *cobra.Command) bool { return c.Name() == "cluster" }) - clusterCmd.PersistentFlags().String("project", preferences.Preferences.Kube.DefaultProject, "Name or ID of project") + clusterCmd.PersistentFlags().String("project", "", "Name or ID of project") _ = flags.MarkAsNoForward(clusterCmd.PersistentFlags(), "project") clusterCmd.PersistentPreRunE = clusterArgToID clusterCreateCmd, _ := lo.Find(clusterCmd.Commands(), func(c *cobra.Command) bool { return c.Name() == "create" }) - _ = flags.SetDefault(clusterCreateCmd.Flags(), "project", preferences.Preferences.Kube.DefaultProject) + _ = flags.SetDefault(clusterCreateCmd.Flags(), "project", "") // Remap to projectCmd, _ := lo.Find(oksCmd.Commands(), func(c *cobra.Command) bool { return c.Name() == "project" }) @@ -116,6 +118,7 @@ func projectArgToID(cmd *cobra.Command, args []string) error { if cmd.Name() == "use" { return nil } + debug.Println("projectArgToID") p := loadProfile(cmd) cl, err := oks.NewClient(p, sdkOptions(cmd)...) @@ -168,6 +171,9 @@ func flagNamesToID(cmd *cobra.Command, args []string) error { } func projectNameToID(ctx context.Context, name string, cl *oks.Client) (string, error) { + if name == "" { + name = prerun.PreferencesFrom(ctx).Kube.DefaultProject + } if name == "" { return "", nil } @@ -222,19 +228,24 @@ func clusterNameToID(ctx context.Context, name, project string, cl *oks.Client) } } -func useProject(c *cobra.Command, args []string) { +func useProject(cmd *cobra.Command, args []string) { var def string if len(args) > 0 { def = args[0] } - preferences.Preferences.Kube.DefaultProject = def - err := preferences.Preferences.Save() + prof, _ := cmd.Flags().GetString("profile") + if prof == "" { + prof = profile.DefaultProfile + } + // retro compatibility + _ = preferences.SetGlobal(func(prefs *preferences.Preferences) { prefs.Kube.DefaultProject = "" }) + err := preferences.Set(prof, func(prefs *preferences.Preferences) { prefs.Kube.DefaultProject = def }) if err != nil { messages.ExitErr(err) } if def == "" { - messages.Success("The default project has been reset") + messages.Success("The default project has been reset for profile %q", prof) } else { - messages.Success("%q is now the default project for all octl kube commands", def) + messages.Success("%q is now the default project for profile %q", def, prof) } } diff --git a/cmd/kube_kubeapi.go b/cmd/kube_kubeapi.go index 428c8a02..d9aed022 100644 --- a/cmd/kube_kubeapi.go +++ b/cmd/kube_kubeapi.go @@ -9,7 +9,6 @@ import ( "github.com/outscale/octl/pkg/config" "github.com/outscale/octl/pkg/flags" "github.com/outscale/octl/pkg/messages" - "github.com/outscale/octl/pkg/preferences" "github.com/outscale/octl/pkg/runner" "github.com/outscale/osc-sdk-go/v3/pkg/oks" "github.com/outscale/osc-sdk-go/v3/pkg/osc" @@ -34,7 +33,7 @@ func buildKubeAPI[Client any](provider string, cmd, parent *cobra.Command, getcl _ = child.MarkPersistentFlagRequired("cluster") } else { child.Flags().String("cluster", "", "[REQUIRED] Name or ID of cluster") - child.Flags().String("project", preferences.Preferences.Kube.DefaultProject, "Name or ID of project") + child.Flags().String("project", "", "Name or ID of project") _ = child.MarkFlagRequired("cluster") _ = flags.MarkAsNoForward(child.Flags(), "project") } diff --git a/cmd/kube_kubectl.go b/cmd/kube_kubectl.go index 0365ed75..e8d80bfb 100644 --- a/cmd/kube_kubectl.go +++ b/cmd/kube_kubectl.go @@ -14,7 +14,6 @@ import ( "github.com/outscale/octl/pkg/debug" "github.com/outscale/octl/pkg/flags" "github.com/outscale/octl/pkg/messages" - "github.com/outscale/octl/pkg/preferences" "github.com/outscale/osc-sdk-go/v3/pkg/oks" "github.com/spf13/cobra" "k8s.io/client-go/tools/clientcmd" @@ -120,6 +119,6 @@ func init() { oksCmd.AddCommand(kubectlCmd) kubectlCmd.Flags().String("cluster", "", "Name or ID of cluster") _ = kubectlCmd.MarkFlagRequired("cluster") - kubectlCmd.Flags().String("project", preferences.Preferences.Kube.DefaultProject, "Name or ID of project") + kubectlCmd.Flags().String("project", "", "Name or ID of project") _ = flags.MarkAsNoForward(kubectlCmd.Flags(), "project") } diff --git a/cmd/kube_test.go b/cmd/kube_test.go index 68b395a3..848a8588 100644 --- a/cmd/kube_test.go +++ b/cmd/kube_test.go @@ -90,4 +90,26 @@ func TestKube(t *testing.T) { t.Run("Kubectl fails with an invalid --project", func(t *testing.T) { runWithError(t, []string{"kube", "kubectl", "--cluster", cluster, "--project", "foobarbaz", "--", "get", "nodes"}, nil) }) + + t.Run("A default project can be set", func(t *testing.T) { + _ = run(t, []string{"kube", "project", "use", emptyProject}, nil) + t.Cleanup(func() { + _ = run(t, []string{"kube", "project", "use"}, nil) + }) + runWithError(t, []string{"kube", "kubectl", "--cluster", cluster, "--", "get", "nodes"}, nil) + }) + t.Run("A default project is ignored if flag si set", func(t *testing.T) { + _ = run(t, []string{"kube", "project", "use", emptyProject}, nil) + t.Cleanup(func() { + _ = run(t, []string{"kube", "project", "use"}, nil) + }) + _ = run(t, []string{"kube", "kubectl", "--cluster", cluster, "--project", project, "--", "get", "nodes"}, nil) + }) + t.Run("A default project is ignored when set on another profile", func(t *testing.T) { + _ = run(t, []string{"kube", "project", "use", emptyProject, "--profile", "foo"}, nil) + t.Cleanup(func() { + _ = run(t, []string{"kube", "project", "use", "--profile", "foo"}, nil) + }) + _ = run(t, []string{"kube", "kubectl", "--cluster", cluster, "--", "get", "nodes"}, nil) + }) } diff --git a/cmd/prerun/preferences.go b/cmd/prerun/preferences.go new file mode 100644 index 00000000..26c8156b --- /dev/null +++ b/cmd/prerun/preferences.go @@ -0,0 +1,36 @@ +package prerun + +import ( + "context" + + "github.com/outscale/octl/pkg/messages" + "github.com/outscale/octl/pkg/preferences" + "github.com/outscale/osc-sdk-go/v3/pkg/profile" + "github.com/spf13/cobra" +) + +type preferenceKey struct{} + +func contextWithPreferences(ctx context.Context, p preferences.Preferences) context.Context { + return context.WithValue(ctx, preferenceKey{}, p) +} + +func PreferencesFrom(ctx context.Context) preferences.Preferences { + p := ctx.Value(preferenceKey{}) + if p == nil { + return preferences.Preferences{} + } + return p.(preferences.Preferences) +} + +func LoadPreferences(cmd *cobra.Command) { + prof, _ := cmd.Flags().GetString("profile") + if prof == "" { + prof = profile.DefaultProfile + } + prefs, err := preferences.Get(prof) + if err != nil { + messages.Err(err.Error()) + } + cmd.SetContext(contextWithPreferences(cmd.Context(), prefs)) +} diff --git a/cmd/profile_test.go b/cmd/profile_test.go index 0993d81c..23d2db1c 100644 --- a/cmd/profile_test.go +++ b/cmd/profile_test.go @@ -201,3 +201,7 @@ func TestProfilePriority(t *testing.T) { assert.Equal(t, "priority_region", p.Region) }) } + +func TestProfilePreferences(t *testing.T) { + +} \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index 18916b19..16d7b3bb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -48,6 +48,7 @@ var rootCmd = &cobra.Command{ PersistentPreRun: func(cmd *cobra.Command, args []string) { prerun.CheckFalse(cmd, args) prerun.CheckUpdate(cmd, args) + prerun.LoadPreferences(cmd) }, Run: root, SilenceErrors: true, // do not display errors when an error occurred, we do it diff --git a/pkg/preferences/preferences.go b/pkg/preferences/preferences.go index d85de75a..91f9846f 100644 --- a/pkg/preferences/preferences.go +++ b/pkg/preferences/preferences.go @@ -2,10 +2,12 @@ package preferences import ( "errors" + "fmt" "io/fs" "os" "path/filepath" + "dario.cat/mergo" "github.com/goccy/go-yaml" "github.com/outscale/octl/pkg/debug" "github.com/outscale/octl/pkg/messages" @@ -15,36 +17,77 @@ type Kube struct { DefaultProject string `yaml:"default_project,omitempty"` } -type All struct { +type Preferences struct { Kube Kube `yaml:"kube,omitzero"` } -var Preferences All +type File struct { + Global Preferences `yaml:"global,omitzero"` + PerProfile map[string]Preferences `yaml:"per_profile,omitzero"` +} -func init() { +func Load() (File, error) { root, err := os.UserConfigDir() if err != nil { debug.Println("Unable to compute user preferences dir", err) - return + return File{}, err } path := filepath.Join(root, "octl", "preferences.yaml") fd, err := os.Open(path) //nolint:gosec if errors.Is(err, fs.ErrNotExist) { debug.Println("no user config file found", path) - return + return File{}, err } if err != nil { messages.Err("Unable to open preferences path: %w") - return + return File{}, err } debug.Println("loading user config from", path) - err = yaml.NewDecoder(fd).Decode(&Preferences) + var pf File + err = yaml.NewDecoder(fd).Decode(&pf) + if err != nil { + return File{}, err + } + return pf, nil +} + +func Get(profile string) (Preferences, error) { + pf, err := Load() + if err != nil { + return Preferences{}, fmt.Errorf("unable to load preferences: %w", err) + } + if pf.PerProfile == nil { + return pf.Global, nil + } + prefs := pf.PerProfile[profile] + mergo.Merge(&prefs, pf.Global) + return prefs, nil +} + +func Set(profile string, fn func(prefs *Preferences)) error { + pf, err := Load() + if err != nil { + return fmt.Errorf("unable to load preferences: %w", err) + } + if pf.PerProfile == nil { + pf.PerProfile = map[string]Preferences{} + } + pref := pf.PerProfile[profile] + fn(&pref) + pf.PerProfile[profile] = pref + return pf.Save() +} + +func SetGlobal(fn func(prefs *Preferences)) error { + pf, err := Load() if err != nil { - messages.Err("Unable to load preferences: %w") + return fmt.Errorf("unable to load preferences: %w", err) } + fn(&pf.Global) + return pf.Save() } -func (p *All) Save() (err error) { +func (pf *File) Save() (err error) { root, err := os.UserConfigDir() if err != nil { debug.Println("Unable to compute user preferences dir", err) @@ -67,5 +110,5 @@ func (p *All) Save() (err error) { } }() debug.Println("saving user preferences to", path) - return yaml.NewEncoder(fd).Encode(p) + return yaml.NewEncoder(fd).Encode(pf) }