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

[feat:] Added Config Command to Kitops CLI #523

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
39 changes: 32 additions & 7 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"path/filepath"

"kitops/pkg/cmd/config"
"kitops/pkg/cmd/dev"
"kitops/pkg/cmd/info"
"kitops/pkg/cmd/inspect"
Expand Down Expand Up @@ -55,6 +56,33 @@ func RunCommand() *cobra.Command {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
output.SetOut(cmd.OutOrStdout())
output.SetErr(cmd.ErrOrStderr())

// Load config from the file (or default if it doesn't exist)
configHome, err := getConfigHome(opts)
if err != nil {
output.Errorf("Failed to read base config directory")
output.Infof("Use the --config flag or set the $%s environment variable to provide a default", constants.KitopsHomeEnvVar)
output.Debugf("Error: %s", err)
return errors.New("exit")
}

configPath := filepath.Join(configHome, constants.ConfigFileName)
amisevsk marked this conversation as resolved.
Show resolved Hide resolved
cfg, err := config.LoadConfig(configPath)
if err != nil {
if !os.IsNotExist(err) { // If the config file exists but there's an error, report it
return output.Fatalf("Failed to load config: %s", err)
}
cfg = config.DefaultConfig() // Load default config if file doesn't exist
amisevsk marked this conversation as resolved.
Show resolved Hide resolved
}

// Override the config values with flag values if the flags were provided
if opts.loglevel == "" && cfg.LogLevel != "" {
opts.loglevel = cfg.LogLevel
}
if opts.progressBars == "" && cfg.Progress != "" {
opts.progressBars = cfg.Progress
}

if err := output.SetLogLevelFromString(opts.loglevel); err != nil {
return output.Fatalln(err)
}
Expand All @@ -75,13 +103,6 @@ func RunCommand() *cobra.Command {
output.SetProgressBars("none")
}

configHome, err := getConfigHome(opts)
if err != nil {
output.Errorf("Failed to read base config directory")
output.Infof("Use the --config flag or set the $%s environment variable to provide a default", constants.KitopsHomeEnvVar)
output.Debugf("Error: %s", err)
return errors.New("exit")
}
ctx := context.WithValue(cmd.Context(), constants.ConfigKey{}, configHome)
cmd.SetContext(ctx)

Expand Down Expand Up @@ -129,6 +150,7 @@ func addSubcommands(rootCmd *cobra.Command) {
rootCmd.AddCommand(pull.PullCommand())
rootCmd.AddCommand(tag.TagCommand())
rootCmd.AddCommand(list.ListCommand())
rootCmd.AddCommand(config.ConfigCommand())
rootCmd.AddCommand(inspect.InspectCommand())
rootCmd.AddCommand(info.InfoCommand())
rootCmd.AddCommand(remove.RemoveCommand())
Expand All @@ -148,6 +170,7 @@ func Execute() {
}

func getConfigHome(opts *rootOptions) (string, error) {
// First check if the config path is provided via flags
if opts.configHome != "" {
output.Debugf("Using config directory from flag: %s", opts.configHome)
amisevsk marked this conversation as resolved.
Show resolved Hide resolved
absHome, err := filepath.Abs(opts.configHome)
Expand All @@ -157,6 +180,7 @@ func getConfigHome(opts *rootOptions) (string, error) {
return absHome, nil
}

// Then check if it's provided via environment variable
envHome := os.Getenv(constants.KitopsHomeEnvVar)
if envHome != "" {
output.Debugf("Using config directory from environment variable: %s", envHome)
Expand All @@ -167,6 +191,7 @@ func getConfigHome(opts *rootOptions) (string, error) {
return absHome, nil
}

// Finally, fall back to the default path
defaultHome, err := constants.DefaultConfigPath()
if err != nil {
return "", err
Expand Down
144 changes: 144 additions & 0 deletions pkg/cmd/config/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2024 The KitOps Authors.
//
// 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.
//
// SPDX-License-Identifier: Apache-2.0
package config

import (
"context"
"fmt"
"kitops/pkg/lib/constants"
"kitops/pkg/output"

"github.com/spf13/cobra"
)

const (
shortDesc = `Manage configuration for KitOps CLI`
longDesc = `Allows setting, getting, listing, and resetting configuration options for the KitOps CLI.

This command provides functionality to manage configuration settings such as
storage paths, credentials file location, CLI version, and update notification preferences.
The configuration values can be set using specific keys, retrieved for inspection, listed,
or reset to default values.`

example = `# Set a configuration option
kit config set storageSubpath /path/to/storage

# Get a configuration option
kit config get storageSubpath

# List all configuration options
kit config list

# Reset configuration to default values
kit config reset`
)

func (opts *configOptions) complete(ctx context.Context, args []string) error {
if len(args) == 0 {
return fmt.Errorf("no configuration key provided")
}
opts.key = args[0]
SkySingh04 marked this conversation as resolved.
Show resolved Hide resolved

if len(args) > 1 {
opts.value = args[1]
}

configHome, ok := ctx.Value(constants.ConfigKey{}).(string)
if !ok {
return fmt.Errorf("default config path not set on command context")
}
opts.configHome = configHome

return nil
}

// ConfigCommand represents the config command
func ConfigCommand() *cobra.Command {
opts := &configOptions{}

cmd := &cobra.Command{
Use: "config [set|get|list|reset] KEY [VALUE]",
Short: shortDesc,
Long: longDesc,
Example: example,
RunE: runCommand(opts),
}

cmd.Args = cobra.MinimumNArgs(1)
cmd.Flags().SortFlags = false
SkySingh04 marked this conversation as resolved.
Show resolved Hide resolved

return cmd
}

func runCommand(opts *configOptions) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

// Handle each command and its required/optional arguments
switch args[0] {
case "set":
if len(args) < 3 {
return output.Fatalf("Missing key or value for 'set'. Usage: kit config set <key> <value>")
}
opts.key, opts.value = args[1], args[2]
if err := opts.complete(ctx, args); err != nil {
return output.Fatalf("Invalid arguments: %s", err)
}
if err := setConfig(ctx, opts); err != nil {
return output.Fatalf("Failed to set config: %s", err)
}
output.Infof("Configuration key '%s' set to '%s'", opts.key, opts.value)

case "get":
if len(args) < 2 {
return output.Fatalf("Missing key for 'get'. Usage: kit config get <key>")
}
opts.key = args[1]
if err := opts.complete(ctx, args); err != nil {
return output.Fatalf("Invalid arguments: %s", err)
}
value, err := getConfig(ctx, opts)
if err != nil {
return output.Fatalf("Failed to get config: %s", err)
}
output.Infof("Configuration key '%s': '%s'", opts.key, value)

case "list":
// No key required for 'list'
if err := opts.complete(ctx, args); err != nil {
return output.Fatalf("Invalid arguments: %s", err)
}
if err := listConfig(ctx, opts); err != nil {
return output.Fatalf("Failed to list configs: %s", err)
}

case "reset":
// No key required for 'reset'
if err := opts.complete(ctx, args); err != nil {
return output.Fatalf("Invalid arguments: %s", err)
}
if err := resetConfig(ctx, opts); err != nil {
return output.Fatalf("Failed to reset config: %s", err)
}
output.Infof("Configuration reset to default values")

default:
return output.Fatalf("Unknown command: %s. Available commands are: set, get, list, reset", args[0])
}

return nil
}
}
163 changes: 163 additions & 0 deletions pkg/cmd/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright 2024 The KitOps Authors.
//
// 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.
//
// SPDX-License-Identifier: Apache-2.0
package config

import (
"context"
"encoding/json"
"fmt"
"kitops/pkg/output"
"os"
"path/filepath"
"reflect"
"strings"
)

type Config struct {
LogLevel string `json:"log_level"`
amisevsk marked this conversation as resolved.
Show resolved Hide resolved
Progress string `json:"progress"`
ConfigDir string `json:"config_dir"`
amisevsk marked this conversation as resolved.
Show resolved Hide resolved
}

// DefaultConfig returns a Config struct with default values.
func DefaultConfig() *Config {
return &Config{
LogLevel: output.LogLevelInfo.String(),
Progress: "plain",
ConfigDir: "",
}
}

// Set a configuration key and value.
func setConfig(_ context.Context, opts *configOptions) error {
configPath := getConfigPath(opts.profile)
cfg, err := LoadConfig(configPath)
if err != nil {
cfg = DefaultConfig() // Start with defaults if config doesn't exist.
}
amisevsk marked this conversation as resolved.
Show resolved Hide resolved

v := reflect.ValueOf(cfg).Elem().FieldByName(strings.Title(opts.key))
SkySingh04 marked this conversation as resolved.
Show resolved Hide resolved
if !v.IsValid() {
return fmt.Errorf("unknown configuration key: %s", opts.key)
}

v.SetString(opts.value)
err = SaveConfig(cfg, configPath)
if err != nil {
return err
}
fmt.Printf("Config '%s' set to '%s'\n", opts.key, opts.value)
return nil
}

// Get a configuration value.
func getConfig(_ context.Context, opts *configOptions) (string, error) {
configPath := getConfigPath(opts.profile)
cfg, err := LoadConfig(configPath)
if err != nil {
return "", err
}

v := reflect.ValueOf(cfg).Elem().FieldByName(strings.Title(opts.key))
SkySingh04 marked this conversation as resolved.
Show resolved Hide resolved
if !v.IsValid() {
return "", fmt.Errorf("unknown configuration key: %s", opts.key)
}

return fmt.Sprintf("%v", v.Interface()), nil
}

// List all configuration values.
func listConfig(_ context.Context, opts *configOptions) error {
configPath := getConfigPath(opts.profile)
cfg, err := LoadConfig(configPath)
if err != nil {
return err
}

// Use reflection to iterate through fields and print them.
v := reflect.ValueOf(cfg).Elem()
t := v.Type()
for i := 0; i < v.NumField(); i++ {
fmt.Printf("%s: %v\n", t.Field(i).Name, v.Field(i).Interface())
}
return nil
}

// Reset configuration to defaults.
func resetConfig(_ context.Context, opts *configOptions) error {
configPath := getConfigPath(opts.profile)
cfg := DefaultConfig()
err := SaveConfig(cfg, configPath)
if err != nil {
return err
}
fmt.Println("Configuration reset to default values.")
return nil
}

// Load configuration from a file.
func LoadConfig(configPath string) (*Config, error) {
if configPath == "" {
return nil, fmt.Errorf("config path is empty")
}

file, err := os.Open(configPath)
if err != nil {
if os.IsNotExist(err) {
return DefaultConfig(), nil // Return default config if file doesn't exist.
}
return nil, err
}
defer file.Close()

var config Config
if err := json.NewDecoder(file).Decode(&config); err != nil {
return nil, err
}
return &config, nil
}

// Save configuration to a file.
func SaveConfig(config *Config, configPath string) error {
file, err := os.Create(configPath)
if err != nil {
return err
}
defer file.Close()

return json.NewEncoder(file).Encode(config)
}

// Get the config path, either from the profile or default.
func getConfigPath(profile string) string {
configDir := os.Getenv("KITOPS_HOME")
if configDir == "" {
homeDir, _ := os.UserHomeDir()
configDir = filepath.Join(homeDir, ".kitops")
}
if profile != "" {
configDir = filepath.Join(configDir, "profiles", profile)
}
return filepath.Join(configDir, "config.json")
}

// ConfigOptions struct to store command options.
type configOptions struct {
key string
value string
profile string
configHome string
}
Loading