Skip to content

Commit

Permalink
Implement set-project subcommand (#3656)
Browse files Browse the repository at this point in the history
This allows us to easily switch between projects using the ID. Note that
there a further PR will allow us to switch between projects using the
name of an immediate child.

Signed-off-by: Juan Antonio Osorio <ozz@stacklok.com>
  • Loading branch information
JAORMX committed Jun 19, 2024
1 parent bd376ce commit 29d4ad9
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 24 deletions.
18 changes: 2 additions & 16 deletions cmd/cli/app/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package app
import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/spf13/cobra"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand All @@ -151,7 +137,7 @@ func initConfig() {
// use defaults
viper.SetConfigName("config")
viper.AddConfigPath(".")
if cfgDirPath != "" {
if cfgDirPath := cli.GetDefaultCLIConfigPath(); cfgDirPath != "" {
viper.AddConfigPath(cfgDirPath)
}
}
Expand Down
131 changes: 131 additions & 0 deletions cmd/cli/app/set_project/set_project.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
6 changes: 4 additions & 2 deletions internal/config/client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions internal/config/client/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
6 changes: 3 additions & 3 deletions internal/config/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions internal/util/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -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(),
))
}

0 comments on commit 29d4ad9

Please sign in to comment.