From 02e409ee7e83ed4edde56ea571c23f67eca4abdb Mon Sep 17 00:00:00 2001 From: Fabian Gonzalez Date: Mon, 25 Aug 2025 14:02:30 -0400 Subject: [PATCH 1/4] port code from https://github.com/kagent-dev/kagent/pull/796 Signed-off-by: Fabian Gonzalez --- go/cli/cmd/kagent/main.go | 11 ++- go/cli/internal/cli/install.go | 110 +++++++++++++++++++++++--- go/cli/internal/profiles/README.md | 9 +++ go/cli/internal/profiles/demo.yaml | 23 ++++++ go/cli/internal/profiles/minimal.yaml | 23 ++++++ go/cli/internal/profiles/profiles.go | 27 +++++++ helm/README.md | 2 - 7 files changed, 191 insertions(+), 14 deletions(-) create mode 100644 go/cli/internal/profiles/README.md create mode 100644 go/cli/internal/profiles/demo.yaml create mode 100644 go/cli/internal/profiles/minimal.yaml create mode 100644 go/cli/internal/profiles/profiles.go diff --git a/go/cli/cmd/kagent/main.go b/go/cli/cmd/kagent/main.go index 210bbbe71..98265f42b 100644 --- a/go/cli/cmd/kagent/main.go +++ b/go/cli/cmd/kagent/main.go @@ -9,6 +9,7 @@ import ( "github.com/abiosoft/ishell/v2" "github.com/kagent-dev/kagent/go/cli/internal/cli" "github.com/kagent-dev/kagent/go/cli/internal/config" + "github.com/kagent-dev/kagent/go/cli/internal/profiles" "github.com/kagent-dev/kagent/go/pkg/client" "github.com/spf13/cobra" ) @@ -32,14 +33,19 @@ func main() { rootCmd.PersistentFlags().StringVarP(&cfg.Namespace, "namespace", "n", "kagent", "Namespace") rootCmd.PersistentFlags().StringVarP(&cfg.OutputFormat, "output-format", "o", "table", "Output format") rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", false, "Verbose output") + var profile string installCmd := &cobra.Command{ Use: "install", Short: "Install kagent", Long: `Install kagent`, Run: func(cmd *cobra.Command, args []string) { - cli.InstallCmd(cmd.Context(), cfg) + cli.InstallCmd(cmd.Context(), cfg, profile) }, } + installCmd.Flags().StringVar(&profile, "profile", "", "Installation profile (minimal|demo)") + _ = installCmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return profiles.Profiles, cobra.ShellCompDirectiveNoFileComp + }) uninstallCmd := &cobra.Command{ Use: "uninstall", @@ -434,8 +440,7 @@ Example: Aliases: []string{"i"}, Help: "Install kagent.", Func: func(c *ishell.Context) { - cfg := config.GetCfg(c) - if pf := cli.InstallCmd(ctx, cfg); pf != nil { + if pf := cli.InteractiveInstallCmd(ctx, c); pf != nil { // Set the port-forward to the shell. shell.Set(portForwardKey, pf) } diff --git a/go/cli/internal/cli/install.go b/go/cli/internal/cli/install.go index d48aab5e4..75dd21d32 100644 --- a/go/cli/internal/cli/install.go +++ b/go/cli/internal/cli/install.go @@ -3,6 +3,7 @@ package cli import ( "context" "fmt" + "github.com/kagent-dev/kagent/go/api/v1alpha1" "os" "os/exec" "strings" @@ -10,12 +11,14 @@ import ( "github.com/kagent-dev/kagent/go/internal/version" + "github.com/abiosoft/ishell/v2" "github.com/briandowns/spinner" "github.com/kagent-dev/kagent/go/cli/internal/config" + "github.com/kagent-dev/kagent/go/cli/internal/profiles" ) // installChart installs or upgrades a Helm chart with the given parameters -func installChart(ctx context.Context, chartName string, namespace string, registry string, version string, setValues []string, s *spinner.Spinner) (string, error) { +func installChart(ctx context.Context, chartName string, namespace string, registry string, version string, setValues []string, inlineValues string) (string, error) { args := []string{ "upgrade", "--install", @@ -35,19 +38,25 @@ func installChart(ctx context.Context, chartName string, namespace string, regis // Add set values if any for _, setValue := range setValues { - if setValue != "" { - args = append(args, "--set", setValue) - } + args = append(args, "--set", setValue) } cmd := exec.CommandContext(ctx, "helm", args...) + + // If a profile is provided, pass the embedded YAML to the stdin of the helm command. + // This must be the last set of arguments. + if inlineValues != "" { + cmd.Stdin = strings.NewReader(inlineValues) + cmd.Args = append(cmd.Args, "-f", "-") + } + if byt, err := cmd.CombinedOutput(); err != nil { return string(byt), err } return "", nil } -func InstallCmd(ctx context.Context, cfg *config.Config) *PortForward { +func InstallCmd(ctx context.Context, cfg *config.Config, profile string) *PortForward { if version.Version == "dev" { fmt.Fprintln(os.Stderr, "Installation requires released version of kagent") return nil @@ -56,6 +65,44 @@ func InstallCmd(ctx context.Context, cfg *config.Config) *PortForward { // get model provider from KAGENT_DEFAULT_MODEL_PROVIDER environment variable or use DefaultModelProvider modelProvider := GetModelProvider() + // If model provider is openai, check if the API key is set + apiKeyName := GetProviderAPIKey(modelProvider) + apiKeyValue := os.Getenv(apiKeyName) + + if apiKeyName != "" && apiKeyValue == "" { + fmt.Fprintf(os.Stderr, "%s is not set\n", apiKeyName) + fmt.Fprintf(os.Stderr, "Please set the %s environment variable\n", apiKeyName) + return nil + } + + helmConfig := setupHelmConfig(modelProvider, apiKeyValue) + // Validate and normalize profile input + profile = strings.TrimSpace(profile) + switch profile { + case "", profiles.ProfileDemo, profiles.ProfileMinimal: + // valid, no change + default: + fmt.Fprintln(os.Stderr, "Invalid --profile value, defaulting to minimal") + profile = profiles.ProfileMinimal + } + if profile != "" { + helmConfig.inlineValues = profiles.GetProfile(profile) + } + + return install(ctx, cfg, helmConfig, modelProvider) +} + +func InteractiveInstallCmd(ctx context.Context, c *ishell.Context) *PortForward { + if version.Version == "dev" { + fmt.Fprintln(os.Stderr, "Installation requires released version of kagent") + return nil + } + + cfg := config.GetCfg(c) + + // get model provider from KAGENT_DEFAULT_MODEL_PROVIDER environment variable or use DefaultModelProvider + modelProvider := GetModelProvider() + //if model provider is openai, check if the api key is set apiKeyName := GetProviderAPIKey(modelProvider) apiKeyValue := os.Getenv(apiKeyName) @@ -66,6 +113,30 @@ func InstallCmd(ctx context.Context, cfg *config.Config) *PortForward { return nil } + helmConfig := setupHelmConfig(modelProvider, apiKeyValue) + + // Add profile selection + profileIdx := c.MultiChoice(profiles.Profiles, "Select a profile:") + selectedProfile := profiles.Profiles[profileIdx] + + helmConfig.inlineValues = profiles.GetProfile(selectedProfile) + + return install(ctx, cfg, helmConfig, modelProvider) +} + +// helmConfig is the config for the kagent chart +type helmConfig struct { + registry string + version string + // values are values which are passed in via --set flags + values []string + // inlineValues are values which are passed in via stdin (e.g. embedded profile YAML) + inlineValues string +} + +// setupHelmConfig sets up the helm config for the kagent chart +// This sets up the general configuration for a helm installation without the profile, which is calculated later based on the installation type (interactive or non-interactive) +func setupHelmConfig(modelProvider v1alpha1.ModelProvider, apiKeyValue string) helmConfig { // Build Helm values helmProviderKey := GetModelProviderHelmValuesKey(modelProvider) values := []string{ @@ -82,14 +153,23 @@ func InstallCmd(ctx context.Context, cfg *config.Config) *PortForward { extraValues := strings.Split(helmExtraArgs, "--set") values = append(values, extraValues...) + return helmConfig{ + registry: helmRegistry, + version: helmVersion, + values: values, + } +} + +// install installs kagent and kagent-crds using the helm config +func install(ctx context.Context, cfg *config.Config, helmConfig helmConfig, modelProvider v1alpha1.ModelProvider) *PortForward { // spinner for installation progress s := spinner.New(spinner.CharSets[35], 100*time.Millisecond) // First install kagent-crds - s.Suffix = " Installing kagent-crds from " + helmRegistry + s.Suffix = " Installing kagent-crds from " + helmConfig.registry defer s.Stop() s.Start() - if output, err := installChart(ctx, "kagent-crds", cfg.Namespace, helmRegistry, helmVersion, nil, s); err != nil { + if output, err := installChart(ctx, "kagent-crds", cfg.Namespace, helmConfig.registry, helmConfig.version, nil, ""); err != nil { // Always stop the spinner before printing error messages s.Stop() @@ -108,8 +188,20 @@ func InstallCmd(ctx context.Context, cfg *config.Config) *PortForward { } // Update status - s.Suffix = fmt.Sprintf(" Installing kagent [%s] Using %s:%s %v", modelProvider, helmRegistry, helmVersion, extraValues) - if output, err := installChart(ctx, "kagent", cfg.Namespace, helmRegistry, helmVersion, values, s); err != nil { + // Removing api key(s) from printed values + redactedValues := []string{} + for _, value := range helmConfig.values { + if strings.Contains(value, "apiKey") { + // Split the value by "=" and replace the second part with "********" + parts := strings.Split(value, "=") + redactedValues = append(redactedValues, parts[0]+"=********") + } else { + redactedValues = append(redactedValues, value) + } + } + + s.Suffix = fmt.Sprintf(" Installing kagent [%s] Using %s:%s %v", modelProvider, helmConfig.registry, helmConfig.version, redactedValues) + if output, err := installChart(ctx, "kagent", cfg.Namespace, helmConfig.registry, helmConfig.version, helmConfig.values, helmConfig.inlineValues); err != nil { // Always stop the spinner before printing error messages s.Stop() fmt.Fprintln(os.Stderr, "Error installing kagent:", output) diff --git a/go/cli/internal/profiles/README.md b/go/cli/internal/profiles/README.md new file mode 100644 index 000000000..36c6fc182 --- /dev/null +++ b/go/cli/internal/profiles/README.md @@ -0,0 +1,9 @@ +# KAgent Profiles + +KAgent's profiles provide a simpler way to set up KAgent in a configured way based on user needs. + +Currently, there are two profiles: +1. `Demo`: For an installation of kagent that includes all our agents. This is useful for demo purposes and new users. +2. `Minimal`: (default) For an installation that does not include any pre-defined agent. This is useful for users who want to start from scratch. + +**Important**: When adding a new profile or updating a name, make sure to update the proper embeddings for it. diff --git a/go/cli/internal/profiles/demo.yaml b/go/cli/internal/profiles/demo.yaml new file mode 100644 index 000000000..d2b266007 --- /dev/null +++ b/go/cli/internal/profiles/demo.yaml @@ -0,0 +1,23 @@ +# The demo profile installs all kagent agents. +# This is useful for demoing kagent, and for testing. +agents: + k8s-agent: + enabled: true + kgateway-agent: + enabled: true + istio-agent: + enabled: true + promql-agent: + enabled: true + observability-agent: + enabled: true + argo-rollouts-agent: + enabled: true + helm-agent: + enabled: true + cilium-policy-agent: + enabled: true + cilium-manager-agent: + enabled: true + cilium-debug-agent: + enabled: true diff --git a/go/cli/internal/profiles/minimal.yaml b/go/cli/internal/profiles/minimal.yaml new file mode 100644 index 000000000..59cc778d5 --- /dev/null +++ b/go/cli/internal/profiles/minimal.yaml @@ -0,0 +1,23 @@ +# The minimal profile does not install any agents, and is meant as a bare minimum installation for kagent. +# This is useful for users who only want to set up kagent without any extra agents. +agents: + k8s-agent: + enabled: false + kgateway-agent: + enabled: false + istio-agent: + enabled: false + promql-agent: + enabled: false + observability-agent: + enabled: false + argo-rollouts-agent: + enabled: false + helm-agent: + enabled: false + cilium-policy-agent: + enabled: false + cilium-manager-agent: + enabled: false + cilium-debug-agent: + enabled: false diff --git a/go/cli/internal/profiles/profiles.go b/go/cli/internal/profiles/profiles.go new file mode 100644 index 000000000..d57b0bbec --- /dev/null +++ b/go/cli/internal/profiles/profiles.go @@ -0,0 +1,27 @@ +package profiles + +import _ "embed" + +//go:embed demo.yaml +var DemoProfile string + +//go:embed minimal.yaml +var MinimalProfile string + +const ( + ProfileDemo = "demo" + ProfileMinimal = "minimal" +) + +var Profiles = []string{ProfileMinimal, ProfileDemo} + +func GetProfile(profile string) string { + switch profile { + case ProfileDemo: + return DemoProfile + case ProfileMinimal: + return MinimalProfile + default: + return MinimalProfile + } +} diff --git a/helm/README.md b/helm/README.md index c7816b400..c439f50f5 100644 --- a/helm/README.md +++ b/helm/README.md @@ -7,8 +7,6 @@ These Helm charts install kagent-crds,kagent, it is required that the Kagent CRD ### Using Helm ```bash -helm install kmcp-crds oci://ghcr.io/kagent-dev/kmcp/helm/kmcp-crds --version 0.1.2 --namespace kagent - # First, install the required CRDs helm install kagent-crds ./helm/kagent-crds/ --namespace kagent From fcbe33e4b2c875313530fa1f47dc92e98b1d8832 Mon Sep 17 00:00:00 2001 From: Fabian Gonzalez Date: Mon, 25 Aug 2025 14:05:14 -0400 Subject: [PATCH 2/4] undo code change Signed-off-by: Fabian Gonzalez --- go/cli/internal/cli/install.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go/cli/internal/cli/install.go b/go/cli/internal/cli/install.go index 75dd21d32..c40861d90 100644 --- a/go/cli/internal/cli/install.go +++ b/go/cli/internal/cli/install.go @@ -38,7 +38,9 @@ func installChart(ctx context.Context, chartName string, namespace string, regis // Add set values if any for _, setValue := range setValues { - args = append(args, "--set", setValue) + if setValue != "" { + args = append(args, "--set", setValue) + } } cmd := exec.CommandContext(ctx, "helm", args...) From f6c3cf3924e03b0e208269a6443164fdc1886919 Mon Sep 17 00:00:00 2001 From: Fabian Gonzalez Date: Mon, 25 Aug 2025 14:30:26 -0400 Subject: [PATCH 3/4] copilot review feedback Signed-off-by: Fabian Gonzalez --- go/cli/internal/cli/install.go | 27 ++++++++++++++------------- go/cli/internal/profiles/profiles.go | 12 ++++++------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/go/cli/internal/cli/install.go b/go/cli/internal/cli/install.go index c40861d90..aa85be9c5 100644 --- a/go/cli/internal/cli/install.go +++ b/go/cli/internal/cli/install.go @@ -3,12 +3,14 @@ package cli import ( "context" "fmt" - "github.com/kagent-dev/kagent/go/api/v1alpha1" "os" "os/exec" + "slices" "strings" "time" + "github.com/kagent-dev/kagent/go/api/v1alpha1" + "github.com/kagent-dev/kagent/go/internal/version" "github.com/abiosoft/ishell/v2" @@ -78,17 +80,15 @@ func InstallCmd(ctx context.Context, cfg *config.Config, profile string) *PortFo } helmConfig := setupHelmConfig(modelProvider, apiKeyValue) - // Validate and normalize profile input - profile = strings.TrimSpace(profile) - switch profile { - case "", profiles.ProfileDemo, profiles.ProfileMinimal: - // valid, no change - default: - fmt.Fprintln(os.Stderr, "Invalid --profile value, defaulting to minimal") - profile = profiles.ProfileMinimal - } - if profile != "" { - helmConfig.inlineValues = profiles.GetProfile(profile) + + // setup profile if provided + if profile = strings.TrimSpace(profile); profile != "" { + if !slices.Contains(profiles.Profiles, profile) { + fmt.Fprintf(os.Stderr, "Invalid --profile value (%s), defaulting to demo\n", profile) + profile = profiles.ProfileDemo + } + + helmConfig.inlineValues = profiles.GetProfileYaml(profile) } return install(ctx, cfg, helmConfig, modelProvider) @@ -121,7 +121,7 @@ func InteractiveInstallCmd(ctx context.Context, c *ishell.Context) *PortForward profileIdx := c.MultiChoice(profiles.Profiles, "Select a profile:") selectedProfile := profiles.Profiles[profileIdx] - helmConfig.inlineValues = profiles.GetProfile(selectedProfile) + helmConfig.inlineValues = profiles.GetProfileYaml(selectedProfile) return install(ctx, cfg, helmConfig, modelProvider) } @@ -195,6 +195,7 @@ func install(ctx context.Context, cfg *config.Config, helmConfig helmConfig, mod for _, value := range helmConfig.values { if strings.Contains(value, "apiKey") { // Split the value by "=" and replace the second part with "********" + // This follows the format we're defining the api key values in the helm chart as part of setupHelmConfig(). parts := strings.Split(value, "=") redactedValues = append(redactedValues, parts[0]+"=********") } else { diff --git a/go/cli/internal/profiles/profiles.go b/go/cli/internal/profiles/profiles.go index d57b0bbec..3074eee71 100644 --- a/go/cli/internal/profiles/profiles.go +++ b/go/cli/internal/profiles/profiles.go @@ -3,10 +3,10 @@ package profiles import _ "embed" //go:embed demo.yaml -var DemoProfile string +var DemoProfileYaml string //go:embed minimal.yaml -var MinimalProfile string +var MinimalProfileYaml string const ( ProfileDemo = "demo" @@ -15,13 +15,13 @@ const ( var Profiles = []string{ProfileMinimal, ProfileDemo} -func GetProfile(profile string) string { +func GetProfileYaml(profile string) string { switch profile { case ProfileDemo: - return DemoProfile + return DemoProfileYaml case ProfileMinimal: - return MinimalProfile + return MinimalProfileYaml default: - return MinimalProfile + return DemoProfileYaml } } From 0f0926d695056262864ab7e41767ac99a41a532b Mon Sep 17 00:00:00 2001 From: Fabian Gonzalez Date: Mon, 25 Aug 2025 14:46:10 -0400 Subject: [PATCH 4/4] create InstallCfg for installation options Signed-off-by: Fabian Gonzalez --- go/cli/cmd/kagent/main.go | 10 +++++++--- go/cli/internal/cli/install.go | 23 ++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/go/cli/cmd/kagent/main.go b/go/cli/cmd/kagent/main.go index 98265f42b..a85f184d3 100644 --- a/go/cli/cmd/kagent/main.go +++ b/go/cli/cmd/kagent/main.go @@ -33,16 +33,20 @@ func main() { rootCmd.PersistentFlags().StringVarP(&cfg.Namespace, "namespace", "n", "kagent", "Namespace") rootCmd.PersistentFlags().StringVarP(&cfg.OutputFormat, "output-format", "o", "table", "Output format") rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", false, "Verbose output") - var profile string + + installCfg := &cli.InstallCfg{ + Config: cfg, + } + installCmd := &cobra.Command{ Use: "install", Short: "Install kagent", Long: `Install kagent`, Run: func(cmd *cobra.Command, args []string) { - cli.InstallCmd(cmd.Context(), cfg, profile) + cli.InstallCmd(cmd.Context(), installCfg) }, } - installCmd.Flags().StringVar(&profile, "profile", "", "Installation profile (minimal|demo)") + installCmd.Flags().StringVar(&installCfg.Profile, "profile", "", "Installation profile (minimal|demo)") _ = installCmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return profiles.Profiles, cobra.ShellCompDirectiveNoFileComp }) diff --git a/go/cli/internal/cli/install.go b/go/cli/internal/cli/install.go index aa85be9c5..1f58f0d3d 100644 --- a/go/cli/internal/cli/install.go +++ b/go/cli/internal/cli/install.go @@ -19,6 +19,11 @@ import ( "github.com/kagent-dev/kagent/go/cli/internal/profiles" ) +type InstallCfg struct { + Config *config.Config + Profile string +} + // installChart installs or upgrades a Helm chart with the given parameters func installChart(ctx context.Context, chartName string, namespace string, registry string, version string, setValues []string, inlineValues string) (string, error) { args := []string{ @@ -60,7 +65,7 @@ func installChart(ctx context.Context, chartName string, namespace string, regis return "", nil } -func InstallCmd(ctx context.Context, cfg *config.Config, profile string) *PortForward { +func InstallCmd(ctx context.Context, cfg *InstallCfg) *PortForward { if version.Version == "dev" { fmt.Fprintln(os.Stderr, "Installation requires released version of kagent") return nil @@ -82,16 +87,16 @@ func InstallCmd(ctx context.Context, cfg *config.Config, profile string) *PortFo helmConfig := setupHelmConfig(modelProvider, apiKeyValue) // setup profile if provided - if profile = strings.TrimSpace(profile); profile != "" { - if !slices.Contains(profiles.Profiles, profile) { - fmt.Fprintf(os.Stderr, "Invalid --profile value (%s), defaulting to demo\n", profile) - profile = profiles.ProfileDemo + if cfg.Profile = strings.TrimSpace(cfg.Profile); cfg.Profile != "" { + if !slices.Contains(profiles.Profiles, cfg.Profile) { + fmt.Fprintf(os.Stderr, "Invalid --profile value (%s), defaulting to demo\n", cfg.Profile) + cfg.Profile = profiles.ProfileDemo } - helmConfig.inlineValues = profiles.GetProfileYaml(profile) + helmConfig.inlineValues = profiles.GetProfileYaml(cfg.Profile) } - return install(ctx, cfg, helmConfig, modelProvider) + return install(ctx, cfg.Config, helmConfig, modelProvider) } func InteractiveInstallCmd(ctx context.Context, c *ishell.Context) *PortForward { @@ -193,9 +198,9 @@ func install(ctx context.Context, cfg *config.Config, helmConfig helmConfig, mod // Removing api key(s) from printed values redactedValues := []string{} for _, value := range helmConfig.values { - if strings.Contains(value, "apiKey") { + if strings.Contains(value, "apiKey=") { // Split the value by "=" and replace the second part with "********" - // This follows the format we're defining the api key values in the helm chart as part of setupHelmConfig(). + // This follows the format we're following to define the api key values in the helm chart (providers.{provider}.apiKey=...) parts := strings.Split(value, "=") redactedValues = append(redactedValues, parts[0]+"=********") } else {