Skip to content

Commit 90212ab

Browse files
AkashKumar7902EItanyajmhbh
authored
fix: honor CLI config defaults (kagent-dev#1746)
Fixes kagent-dev#973 ## Summary - Load `~/.kagent/config.yaml` before constructing Cobra commands so saved values become the CLI defaults. - Keep explicit flags as overrides by binding command flags to the same shared config object. - Use the loaded namespace as the deploy `--namespace` default so registering that command does not reset the configured namespace. ## Testing - `go test ./core/cli/cmd/kagent` - `go test ./core/cli/internal/config ./core/cli/cmd/kagent` - `go test ./core/cli/...` - `go test -race -skip 'TestE2E.*' ./core/cli/...` - `go vet ./core/cli/cmd/kagent ./core/cli/internal/config` - `go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run ./core/cli/cmd/kagent ./core/cli/internal/config` - `git diff --check` - Temporary-HOME smoke check: `go run ./core/cli/cmd/kagent --help` shows config-file defaults for `--kagent-url`, `--namespace`, `--output-format`, and `--timeout`. Signed-off-by: Akash Kumar <meakash7902@gmail.com> Co-authored-by: Eitan Yarmush <eitan.yarmush@solo.io> Co-authored-by: J.M. Huibonhoa <jmhbh@users.noreply.github.com>
1 parent 3eaf3be commit 90212ab

2 files changed

Lines changed: 137 additions & 27 deletions

File tree

go/core/cli/cmd/kagent/main.go

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"os"
77
"os/signal"
88
"syscall"
9-
"time"
109

1110
cli "github.com/kagent-dev/kagent/go/core/cli/internal/cli/agent"
1211
"github.com/kagent-dev/kagent/go/core/cli/internal/cli/envdoc"
@@ -34,20 +33,43 @@ func main() {
3433

3534
cancel()
3635
}()
37-
cfg := &config.Config{}
36+
cfg, err := loadConfig()
37+
if err != nil {
38+
fmt.Fprintf(os.Stderr, "Error initializing config: %v\n", err)
39+
os.Exit(1)
40+
}
41+
42+
rootCmd := newRootCommand(ctx, cfg)
43+
if err := rootCmd.ExecuteContext(ctx); err != nil {
44+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
45+
46+
os.Exit(1)
47+
}
48+
}
49+
50+
func loadConfig() (*config.Config, error) {
51+
if err := config.Init(); err != nil {
52+
return nil, err
53+
}
54+
return config.Get()
55+
}
3856

57+
func newRootCommand(ctx context.Context, cfg *config.Config) *cobra.Command {
3958
rootCmd := &cobra.Command{
4059
Use: "kagent",
4160
Short: "kagent is a CLI and TUI for kagent",
4261
Long: "kagent is a CLI and TUI for kagent",
43-
Run: runInteractive,
62+
Run: func(cmd *cobra.Command, args []string) {
63+
runInteractive(cmd, args, cfg)
64+
},
4465
}
66+
rootCmd.SetContext(ctx)
4567

46-
rootCmd.PersistentFlags().StringVar(&cfg.KAgentURL, "kagent-url", "http://localhost:8083", "KAgent URL")
47-
rootCmd.PersistentFlags().StringVarP(&cfg.Namespace, "namespace", "n", "kagent", "Namespace")
48-
rootCmd.PersistentFlags().StringVarP(&cfg.OutputFormat, "output-format", "o", "table", "Output format")
49-
rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", false, "Verbose output")
50-
rootCmd.PersistentFlags().DurationVar(&cfg.Timeout, "timeout", 300*time.Second, "Timeout")
68+
rootCmd.PersistentFlags().StringVar(&cfg.KAgentURL, "kagent-url", cfg.KAgentURL, "KAgent URL")
69+
rootCmd.PersistentFlags().StringVarP(&cfg.Namespace, "namespace", "n", cfg.Namespace, "Namespace")
70+
rootCmd.PersistentFlags().StringVarP(&cfg.OutputFormat, "output-format", "o", cfg.OutputFormat, "Output format")
71+
rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", cfg.Verbose, "Verbose output")
72+
rootCmd.PersistentFlags().DurationVar(&cfg.Timeout, "timeout", cfg.Timeout, "Timeout")
5173
installCfg := &cli.InstallCfg{
5274
Config: cfg,
5375
}
@@ -348,7 +370,7 @@ Examples:
348370
// Add flags for deploy command
349371
deployCmd.Flags().StringVarP(&deployCfg.Image, "image", "i", "", "Image to use (defaults to localhost:5001/{agentName}:latest)")
350372
deployCmd.Flags().StringVar(&deployCfg.EnvFile, "env-file", "", "Path to .env file containing environment variables (including API keys)")
351-
deployCmd.Flags().StringVar(&deployCfg.Config.Namespace, "namespace", "kagent", "Kubernetes namespace to deploy to")
373+
deployCmd.Flags().StringVar(&deployCfg.Config.Namespace, "namespace", cfg.Namespace, "Kubernetes namespace to deploy to")
352374
deployCmd.Flags().BoolVar(&deployCfg.DryRun, "dry-run", false, "Output YAML manifests without applying them to the cluster")
353375
deployCmd.Flags().StringVar(&deployCfg.Platform, "platform", "", "Target platform for Docker build (e.g., linux/amd64, linux/arm64)")
354376

@@ -429,26 +451,10 @@ Examples:
429451

430452
rootCmd.AddCommand(installCmd, uninstallCmd, invokeCmd, bugReportCmd, versionCmd, dashboardCmd, getCmd, initCmd, buildCmd, deployCmd, addMcpCmd, runCmd, mcp.NewMCPCmd(), envdoc.NewEnvCmd())
431453

432-
// Initialize config
433-
if err := config.Init(); err != nil {
434-
fmt.Fprintf(os.Stderr, "Error initializing config: %v\n", err)
435-
os.Exit(1)
436-
}
437-
438-
if err := rootCmd.ExecuteContext(ctx); err != nil {
439-
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
440-
441-
os.Exit(1)
442-
}
454+
return rootCmd
443455
}
444456

445-
func runInteractive(cmd *cobra.Command, args []string) {
446-
cfg, err := config.Get()
447-
if err != nil {
448-
fmt.Fprintf(os.Stderr, "Error getting config: %v\n", err)
449-
os.Exit(1)
450-
}
451-
457+
func runInteractive(cmd *cobra.Command, args []string, cfg *config.Config) {
452458
client := cfg.Client()
453459

454460
// Start port forward and ensure it is healthy.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
"time"
9+
10+
"github.com/kagent-dev/kagent/go/core/cli/internal/config"
11+
"github.com/spf13/pflag"
12+
"github.com/spf13/viper"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestLoadConfigReadsConfigFileValues(t *testing.T) {
18+
resetConfigState(t)
19+
20+
homeDir := t.TempDir()
21+
t.Setenv("HOME", homeDir)
22+
23+
configDir := filepath.Join(homeDir, ".kagent")
24+
require.NoError(t, os.MkdirAll(configDir, 0755))
25+
require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(`
26+
kagent_url: http://kagent.example.test
27+
namespace: configured-ns
28+
output_format: json
29+
verbose: true
30+
timeout: 45s
31+
`), 0600))
32+
33+
cfg, err := loadConfig()
34+
require.NoError(t, err)
35+
36+
assert.Equal(t, "http://kagent.example.test", cfg.KAgentURL)
37+
assert.Equal(t, "configured-ns", cfg.Namespace)
38+
assert.Equal(t, "json", cfg.OutputFormat)
39+
assert.True(t, cfg.Verbose)
40+
assert.Equal(t, 45*time.Second, cfg.Timeout)
41+
}
42+
43+
func TestRootCommandUsesConfigValuesAsFlagDefaults(t *testing.T) {
44+
cfg := &config.Config{
45+
KAgentURL: "http://kagent.example.test",
46+
Namespace: "configured-ns",
47+
OutputFormat: "json",
48+
Verbose: true,
49+
Timeout: 45 * time.Second,
50+
}
51+
52+
rootCmd := newRootCommand(context.Background(), cfg)
53+
54+
assert.Equal(t, "http://kagent.example.test", rootCmd.PersistentFlags().Lookup("kagent-url").DefValue)
55+
assert.Equal(t, "configured-ns", rootCmd.PersistentFlags().Lookup("namespace").DefValue)
56+
assert.Equal(t, "json", rootCmd.PersistentFlags().Lookup("output-format").DefValue)
57+
assert.Equal(t, "true", rootCmd.PersistentFlags().Lookup("verbose").DefValue)
58+
assert.Equal(t, "45s", rootCmd.PersistentFlags().Lookup("timeout").DefValue)
59+
60+
deployCmd, _, err := rootCmd.Find([]string{"deploy"})
61+
require.NoError(t, err)
62+
require.NotNil(t, deployCmd)
63+
64+
assert.Equal(t, "configured-ns", deployCmd.Flags().Lookup("namespace").DefValue)
65+
assert.Equal(t, "configured-ns", cfg.Namespace)
66+
}
67+
68+
func TestRootCommandFlagsOverrideConfigValues(t *testing.T) {
69+
cfg := &config.Config{
70+
KAgentURL: "http://kagent.example.test",
71+
Namespace: "configured-ns",
72+
OutputFormat: "json",
73+
Verbose: false,
74+
Timeout: 45 * time.Second,
75+
}
76+
77+
rootCmd := newRootCommand(context.Background(), cfg)
78+
require.NoError(t, rootCmd.ParseFlags([]string{
79+
"--kagent-url", "http://flag.example.test",
80+
"--namespace", "flag-ns",
81+
"--output-format", "yaml",
82+
"--verbose",
83+
"--timeout", "10s",
84+
}))
85+
86+
assert.Equal(t, "http://flag.example.test", cfg.KAgentURL)
87+
assert.Equal(t, "flag-ns", cfg.Namespace)
88+
assert.Equal(t, "yaml", cfg.OutputFormat)
89+
assert.True(t, cfg.Verbose)
90+
assert.Equal(t, 10*time.Second, cfg.Timeout)
91+
}
92+
93+
func resetConfigState(t *testing.T) {
94+
t.Helper()
95+
96+
oldCommandLine := pflag.CommandLine
97+
viper.Reset()
98+
pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError)
99+
100+
t.Cleanup(func() {
101+
viper.Reset()
102+
pflag.CommandLine = oldCommandLine
103+
})
104+
}

0 commit comments

Comments
 (0)