Skip to content

Commit

Permalink
Implement cd subcommand
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 <[email protected]>
  • Loading branch information
JAORMX committed Jun 19, 2024
1 parent 54c62db commit 03db434
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 24 deletions.
130 changes: 130 additions & 0 deletions cmd/cli/app/cd/cd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// 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 version provides the version command for the minder CLI
package version

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"
)

// CDCmd is the cd command
var CDCmd = &cobra.Command{
Use: "cd",
Short: "Move the current context to another project",
Long: `The minder cd 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: cdCommand,
}

// cdCommand is the command for changing the current project
func cdCommand(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(CDCmd)
}
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
1 change: 1 addition & 0 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
_ "github.com/stacklok/minder/cmd/cli/app/auth"
_ "github.com/stacklok/minder/cmd/cli/app/auth/invite"
_ "github.com/stacklok/minder/cmd/cli/app/auth/offline_token"
_ "github.com/stacklok/minder/cmd/cli/app/cd"
_ "github.com/stacklok/minder/cmd/cli/app/docs"
_ "github.com/stacklok/minder/cmd/cli/app/profile"
_ "github.com/stacklok/minder/cmd/cli/app/profile/status"
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 03db434

Please sign in to comment.