Skip to content
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ Use "cloudctl [command] --help" for more information about a command.
## Requirements and Setup
Download the latest release from [here](https://github.com/cloudoperators/cloudctl/releases), move to a location in PATH and update file permissions.

## Passing configuration options
Configuration options for each command can be provided through command line parameters, environment variables (using `CLOUDCTL_` as a variable name prefix), and/or through configuration file.
Configuration file location is searched in that order (first found takes precedence):
* what is provided as a value for `--config` parameter
* what is provided as a value for `$CLOUDCTL_CONFIG` environment variable
* file paths:
* `./.cloudctl.yaml`
* `$HOME/.cloudctl.yaml`
* `./cloudctl.yaml`
* `$HOME/cloudctl.yaml`
* if `$XDG_CONFIG_HOME` is set:
* `$XDG_CONFIG_HOME/cloudctl/cloudctl.yaml`
* `$XDG_CONFIG_HOME/cloudctl.yaml`
* if not, finally falling back to:
* `$HOME/.config/cloudctl/cloudctl.yaml`
* `$HOME/.config/cloudctl.yaml`

## Support, Feedback, Contributing

Expand Down
9 changes: 9 additions & 0 deletions cmd/cluster-version.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
Expand All @@ -31,6 +32,9 @@ var (
)

func runClusterVersion(cmd *cobra.Command, args []string) error {
// Use viper as a source of configuration
kubeconfig = viper.GetString("kubeconfig")
kubecontext = viper.GetString("context")

cfg, err := configWithContext(kubecontext, kubeconfig)
if err != nil {
Expand Down Expand Up @@ -138,4 +142,9 @@ func getUnauthenticatedVersion(cfg *rest.Config) (*version.Info, error) {
func init() {
clusterVersionCmd.Flags().StringVarP(&kubeconfig, "kubeconfig", "k", clientcmd.RecommendedHomeFile, "kubeconfig file path")
Comment thread
onuryilmaz marked this conversation as resolved.
clusterVersionCmd.Flags().StringVarP(&kubecontext, "context", "c", "", "cluster version of the specified context in kubeconfig")

// BindPFlags can theroretically return an error if called with `nil` as an argument
// which should never happened after at least one flag was defined. That's why the output
// there is ignored.
viper.BindPFlags(clusterVersionCmd.Flags())
}
79 changes: 79 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ package cmd

import (
"context"
"os"
"path/filepath"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
Expand All @@ -32,12 +36,26 @@ Examples:
cloudctl version`,
}

var (
configFilePath string
)

// Execute runs the CLI with the provided context.
func Execute(ctx context.Context) error {
return rootCmd.ExecuteContext(ctx)
}

func init() {
cobra.OnInitialize(func() {
cobra.CheckErr(setupConfig())
})
rootCmd.PersistentFlags().StringVar(&configFilePath, "config", "", "Path to configuration file")

// BindPFlags can theroretically return an error if called with `nil` as an argument
// which should never happened after at least one flag was defined. That's why the output
// there is ignored.
viper.BindPFlags(rootCmd.PersistentFlags())

// Add subcommands here
rootCmd.AddCommand(syncCmd)
rootCmd.AddCommand(clusterVersionCmd)
Expand All @@ -55,3 +73,64 @@ func configWithContext(contextName, kubeconfigPath string) (*rest.Config, error)
cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
return cc.ClientConfig()
}

func setupConfig() error {
home, err := os.UserHomeDir()
if err != nil {
return err
}

// Optionally read environment variables, config files, etc.
viper.SetEnvPrefix("CLOUDCTL")
viper.AutomaticEnv()

viper.SetConfigType("yaml")

configFilePath = viper.GetString("config")
if len(configFilePath) > 0 {
// Phase 1
// First we are trying config provided as a command line parameter. Fail if there was an error
// during reading configuration from this specified path.
viper.SetConfigFile(configFilePath)
return viper.ReadInConfig()
} else {
// Phase 2
// Then we are searching for ".cloudctl.yaml" in current or home directory
viper.AddConfigPath(".")
viper.AddConfigPath(home)
// NOTE: viper is automatically adding a file extension basing on the value of called above `SetConfigType`
viper.SetConfigName(".cloudctl")
Comment thread
jellonek marked this conversation as resolved.
}

err = viper.ReadInConfig()
Comment thread
onuryilmaz marked this conversation as resolved.
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Phase 3
// If reading config in above described locations failed, we are looking for configuration
// in these locations:
// locations set in PHASE 2:
// ./cloudctl.yaml
// $HOME/cloudctl.yaml
// if $XDG_CONFIG_HOME is set:
// $XDG_CONFIG_HOME/cloudctl/cloudctl.yaml
// $XDG_CONFIG_HOME/cloudctl.yaml
// else:
// $HOME/.config/cloudctl/cloudctl.yaml
// $HOME/.config/cloudctl.yaml
// NOTE: viper is automatically adding a file extension basing on the value of called above `SetConfigType`
viper.SetConfigName("cloudctl")
if xdgConfig := os.Getenv("XDG_CONFIG_HOME"); len(xdgConfig) > 0 {
viper.AddConfigPath(filepath.Join(xdgConfig, "cloudctl"))
viper.AddConfigPath(xdgConfig)
} else {
viper.AddConfigPath(filepath.Join(home, ".config", "cloudctl"))
viper.AddConfigPath(filepath.Join(home, ".config"))
}
err = viper.ReadInConfig()
// If configuration was not found in any of above listed locations - that's ok.
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
err = nil
}
}

return err
}
79 changes: 79 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"os"
"testing"

"github.com/spf13/viper"

. "github.com/onsi/gomega"
)

var ConfigA = []byte(`kubeconfig: A
config: B
`)

func TestNotSetConfigPath(t *testing.T) {
g := NewWithT(t)

t.Cleanup(func() { viper.Reset() })

// Ensure default config file location is not set
config := rootCmd.PersistentFlags().Lookup("config")
g.Expect(config).NotTo(BeNil())
g.Expect(config.Value.String()).To(BeEmpty())

// ... and that does not lead to an error during configuration setup
g.Expect(setupConfig()).To(BeNil())
}

func TestConfigurationLoad(t *testing.T) {
g := NewWithT(t)

f, err := os.CreateTemp("", "test_cloudctl_config")
g.Expect(err).To(BeNil())
t.Cleanup(func() { os.Remove(f.Name()) })

_, err = f.Write(ConfigA)
g.Expect(err).To(BeNil())
err = f.Close()
g.Expect(err).To(BeNil())

// Set config file location env variable
orig := os.Getenv("CLOUDCTL_CONFIG")
os.Setenv("CLOUDCTL_CONFIG", f.Name())

t.Cleanup(func() {
viper.Reset()
os.Setenv("CLOUDCTL_CONFIG", orig)
})

// Do the setup
g.Expect(setupConfig()).To(BeNil())

// Check if config file variable was not overwriten with data from config file
g.Expect(viper.GetString("config")).To(Equal(f.Name()))

// Check if `kubeconfig` variable was set to the value from temporary file
g.Expect(viper.GetString("kubeconfig")).To(Equal("A"))
}

func TestMissingConfigurationFile(t *testing.T) {
g := NewWithT(t)

// Set config file location env variable
orig := os.Getenv("CLOUDCTL_CONFIG")
os.Setenv("CLOUDCTL_CONFIG", "A")

t.Cleanup(func() {
viper.Reset()
os.Setenv("CLOUDCTL_CONFIG", orig)
})

// Do the setup
err := setupConfig()
g.Expect(err).NotTo(BeNil())
}
19 changes: 19 additions & 0 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
greenhousemetav1alpha1 "github.com/cloudoperators/greenhouse/api/meta/v1alpha1"
"github.com/cloudoperators/greenhouse/api/v1alpha1"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
Expand Down Expand Up @@ -56,6 +57,11 @@ func init() {
syncCmd.Flags().StringVar(&kubeloginPath, "kubelogin-path", "kubelogin", "path to kubelogin command when using exec-plugin auth-type")
syncCmd.Flags().StringSliceVar(&kubeloginExtraArgs, "kubelogin-extra-args", nil, "extra arguments to pass to kubelogin exec plugin")
syncCmd.Flags().StringVar(&kubeloginTokenCacheDir, "kubelogin-token-cache-dir", "$(HOME)/.kube/cache/oidc-login", "token cache directory for kubelogin")

// BindPFlags can theroretically return an error if called with `nil` as an argument
// which should never happened after at least one flag was defined. That's why the output
// there is ignored.
viper.BindPFlags(syncCmd.Flags())
}

var syncCmd = &cobra.Command{
Expand All @@ -65,6 +71,19 @@ var syncCmd = &cobra.Command{
}

func runSync(cmd *cobra.Command, args []string) error {
// Use viper as a source of configuration
greenhouseClusterKubeconfig = viper.GetString("greenhouse-cluster-kubeconfig")
greenhouseClusterContext = viper.GetString("greenhouse-cluster-context")
greenhouseClusterNamespace = viper.GetString("greenhouse-cluster-namespace")
remoteClusterKubeconfig = viper.GetString("remote-cluster-kubeconfig")
remoteClusterName = viper.GetString("remote-cluster-name")
prefix = viper.GetString("prefix")
mergeIdenticalUsers = viper.GetBool("merge-identical-users")
authType = viper.GetString("auth-type")
kubeloginPath = viper.GetString("kubelogin-path")
kubeloginExtraArgs = viper.GetStringSlice("kubelogin-extra-args")
kubeloginTokenCacheDir = viper.GetString("kubelogin-token-cache-dir")

if greenhouseClusterKubeconfig == "" {
return fmt.Errorf("greenhouse cluster kubeconfig path is empty")
}
Expand Down
19 changes: 10 additions & 9 deletions cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"runtime"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var (
Expand All @@ -28,11 +29,6 @@ type versionInfo struct {
Platform string `json:"platform"`
}

var (
versionShort bool
versionJSON bool
)

var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the cloudctl version information",
Expand All @@ -46,7 +42,7 @@ var versionCmd = &cobra.Command{
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
}

if versionJSON {
if viper.GetBool("json") {
b, err := json.MarshalIndent(info, "", " ")
if err != nil {
return err
Expand All @@ -55,7 +51,7 @@ var versionCmd = &cobra.Command{
return nil
}

if versionShort {
if viper.GetBool("short") {
fmt.Println(info.Version)
return nil
}
Expand All @@ -69,6 +65,11 @@ var versionCmd = &cobra.Command{
}

func init() {
versionCmd.Flags().BoolVar(&versionShort, "short", false, "print only the version number")
versionCmd.Flags().BoolVar(&versionJSON, "json", false, "print version information as JSON")
versionCmd.Flags().Bool("short", false, "print only the version number")
versionCmd.Flags().Bool("json", false, "print version information as JSON")

// BindPFlags can theroretically return an error if called with `nil` as an argument
// which should never happened after at least one flag was defined. That's why the output
// there is ignored.
viper.BindPFlags(versionCmd.Flags())
}
6 changes: 0 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@ import (
"os/signal"
"syscall"

"github.com/spf13/viper"

"github.com/cloudoperators/cloudctl/cmd"

_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
)

func main() {
// Optionally read environment variables, config files, etc.
viper.SetEnvPrefix("CLOUDCTL")
viper.AutomaticEnv()

// Graceful cancellation on SIGINT/SIGTERM
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
Expand Down
Loading