Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement set-project subcommand #3656

Merged
merged 1 commit into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(),
))
}