diff --git a/cmd/cli/app/root.go b/cmd/cli/app/root.go index 102698be8e..0b5559e66c 100644 --- a/cmd/cli/app/root.go +++ b/cmd/cli/app/root.go @@ -19,7 +19,6 @@ package app import ( "fmt" "os" - "path/filepath" "strings" "github.com/spf13/cobra" @@ -28,7 +27,6 @@ import ( "github.com/stacklok/minder/internal/config" clientconfig "github.com/stacklok/minder/internal/config/client" "github.com/stacklok/minder/internal/constants" - "github.com/stacklok/minder/internal/util" "github.com/stacklok/minder/internal/util/cli" ) @@ -117,19 +115,7 @@ func initConfig() { viper.SetEnvPrefix("minder") viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) - //nolint:errcheck // ignore error as we are just checking if the file exists - cfgDirPath, _ := util.GetConfigDirPath() - - var xdgConfigPath string - if cfgDirPath != "" { - xdgConfigPath = filepath.Join(cfgDirPath, "config.yaml") - } - - cfgFile := viper.GetString("config") - cfgFilePath := config.GetRelevantCfgPath(append([]string{cfgFile}, - filepath.Join(".", "config.yaml"), - xdgConfigPath, - )) + cfgFilePath := cli.GetRelevantCLIConfigPath(viper.GetViper()) if cfgFilePath != "" { cfgFileData, err := config.GetConfigFileData(cfgFilePath) if err != nil { @@ -151,7 +137,7 @@ func initConfig() { // use defaults viper.SetConfigName("config") viper.AddConfigPath(".") - if cfgDirPath != "" { + if cfgDirPath := cli.GetDefaultCLIConfigPath(); cfgDirPath != "" { viper.AddConfigPath(cfgDirPath) } } diff --git a/cmd/cli/app/set_project/set_project.go b/cmd/cli/app/set_project/set_project.go new file mode 100644 index 0000000000..e2f12a09cc --- /dev/null +++ b/cmd/cli/app/set_project/set_project.go @@ -0,0 +1,131 @@ +// +// Copyright 2023 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package set_project provides the version command for the minder CLI +package set_project + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" + + "github.com/stacklok/minder/cmd/cli/app" + "github.com/stacklok/minder/internal/config" + clientconfig "github.com/stacklok/minder/internal/config/client" + "github.com/stacklok/minder/internal/util/cli" +) + +// SetProjectCmd is the cd command +var SetProjectCmd = &cobra.Command{ + Use: "set-project", + Aliases: []string{"sp", "cd"}, + Short: "Move the current context to another project", + Long: `The minder set-project command moves the current context to another project. +Passing a UUID will move the context to the project with that UUID. This is akin to +using an absolute path in a filesystem.`, + RunE: spCommand, +} + +// spCommand is the command for changing the current project +func spCommand(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return cmd.Usage() + } + + project := args[0] + + _, err := uuid.Parse(project) + // TODO: Implement `cd` to a project name + if err != nil { + return cli.MessageAndError("Error parsing project ID", err) + } + + cfgp := cli.GetRelevantCLIConfigPath(viper.GetViper()) + if cfgp == "" { + // There is no config file at the moment. Let's create one. + cfgp, err = persistEmptyDefaultConfig() + if err != nil { + return cli.MessageAndError("Error creating config file", err) + } + } + + viper.SetConfigFile(cfgp) + if err := viper.ReadInConfig(); err != nil { + return cli.MessageAndError("Error reading config file", err) + } + + cfg, err := config.ReadConfigFromViper[clientconfig.Config](viper.GetViper()) + if err != nil { + return fmt.Errorf("unable to read config: %w", err) + } + + cfg.Project = project + + w, err := os.OpenFile(filepath.Clean(cfgp), os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return cli.MessageAndError("Error opening config file for writing", err) + } + + defer func() { + //nolint:errcheck // leaking file handle is not a concern here + _ = w.Close() + }() + + enc := yaml.NewEncoder(w) + enc.SetIndent(2) + + defer enc.Close() + + if err := enc.Encode(cfg); err != nil { + return cli.MessageAndError("Error encoding config to file", err) + } + + return nil +} + +func persistEmptyDefaultConfig() (string, error) { + cfgp := cli.GetDefaultCLIConfigPath() + if cfgp == "" { + return "", errors.New("no default config path found") + } + f, err := os.Create(filepath.Clean(cfgp)) + if err != nil { + if !errors.Is(err, os.ErrExist) { + return "", err + } + + // File already exists, no need to write the default config + return cfgp, nil + } + // Ensure we've written the default config to the file + if err := f.Sync(); err != nil { + return "", err + } + + //nolint:errcheck // leaking file handle is not a concern here + _ = f.Close() + + return cfgp, nil +} + +func init() { + app.RootCmd.AddCommand(SetProjectCmd) +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 9879b7cb61..5bfb6a428e 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -31,6 +31,7 @@ import ( _ "github.com/stacklok/minder/cmd/cli/app/quickstart" _ "github.com/stacklok/minder/cmd/cli/app/repo" _ "github.com/stacklok/minder/cmd/cli/app/ruletype" + _ "github.com/stacklok/minder/cmd/cli/app/set_project" _ "github.com/stacklok/minder/cmd/cli/app/version" ) diff --git a/internal/config/client/config.go b/internal/config/client/config.go index 9697687572..aefd8946b1 100644 --- a/internal/config/client/config.go +++ b/internal/config/client/config.go @@ -26,8 +26,10 @@ import ( // Config is the configuration for the minder cli type Config struct { - GRPCClientConfig config.GRPCClientConfig `mapstructure:"grpc_server"` - Identity IdentityConfigWrapper `mapstructure:"identity"` + GRPCClientConfig config.GRPCClientConfig `mapstructure:"grpc_server" yaml:"grpc_server" json:"grpc_server"` + Identity IdentityConfigWrapper `mapstructure:"identity" yaml:"identity" json:"identity"` + // Project is the current project + Project string `mapstructure:"project" yaml:"project" json:"project"` } // RegisterMinderClientFlags registers the flags for the minder cli diff --git a/internal/config/client/identity.go b/internal/config/client/identity.go index c37601bd38..07c4f6e1e3 100644 --- a/internal/config/client/identity.go +++ b/internal/config/client/identity.go @@ -17,14 +17,14 @@ package client // IdentityConfigWrapper is the configuration wrapper for the identity provider used by minder-cli type IdentityConfigWrapper struct { - CLI IdentityConfig `mapstructure:"cli"` + CLI IdentityConfig `mapstructure:"cli" yaml:"cli" json:"cli"` } // IdentityConfig is the configuration for the identity provider used by minder-cli type IdentityConfig struct { // IssuerUrl is the base URL where the identity server is running - IssuerUrl string `mapstructure:"issuer_url" default:"https://auth.stacklok.com"` + IssuerUrl string `mapstructure:"issuer_url" default:"https://auth.stacklok.com" yaml:"issuer_url" json:"issuer_url"` // ClientId is the client ID that identifies the server client ID - ClientId string `mapstructure:"client_id" default:"minder-cli"` + ClientId string `mapstructure:"client_id" default:"minder-cli" yaml:"client_id" json:"client_id"` } diff --git a/internal/config/common.go b/internal/config/common.go index b553b08e2b..efccc34aaa 100644 --- a/internal/config/common.go +++ b/internal/config/common.go @@ -119,13 +119,13 @@ func RegisterDatabaseFlags(v *viper.Viper, flags *pflag.FlagSet) error { // GRPCClientConfig is the configuration for a service to connect to minder gRPC server type GRPCClientConfig struct { // Host is the host to connect to - Host string `mapstructure:"host" default:"api.stacklok.com"` + Host string `mapstructure:"host" yaml:"host" json:"host" default:"api.stacklok.com"` // Port is the port to connect to - Port int `mapstructure:"port" default:"443"` + Port int `mapstructure:"port" yaml:"port" json:"port" default:"443"` // Insecure is whether to allow establishing insecure connections - Insecure bool `mapstructure:"insecure" default:"false"` + Insecure bool `mapstructure:"insecure" yaml:"insecure" json:"insecure" default:"false"` } // RegisterGRPCClientConfigFlags registers the flags for the gRPC client diff --git a/internal/util/cli/cli.go b/internal/util/cli/cli.go index 822c29900a..51d2263518 100644 --- a/internal/util/cli/cli.go +++ b/internal/util/cli/cli.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "regexp" "strings" "time" @@ -235,3 +236,30 @@ func ConcatenateAndWrap(input string, maxLen int) string { return result } + +// GetDefaultCLIConfigPath returns the default path for the CLI config file +// Returns an empty string if the path cannot be determined +func GetDefaultCLIConfigPath() string { + //nolint:errcheck // ignore error as we are just checking if the file exists + cfgDirPath, _ := util.GetConfigDirPath() + + var xdgConfigPath string + if cfgDirPath != "" { + xdgConfigPath = filepath.Join(cfgDirPath, "config.yaml") + } + + return xdgConfigPath +} + +// GetRelevantCLIConfigPath returns the relevant CLI config path. +// It will return the first path that exists from the following: +// 1. The path specified in the config flag +// 2. The local config.yaml file +// 3. The default CLI config path +func GetRelevantCLIConfigPath(v *viper.Viper) string { + cfgFile := v.GetString("config") + return config.GetRelevantCfgPath(append([]string{cfgFile}, + filepath.Join(".", "config.yaml"), + GetDefaultCLIConfigPath(), + )) +}