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
15 changes: 12 additions & 3 deletions go/cli/cmd/kagent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -32,14 +33,23 @@ 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")

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)
cli.InstallCmd(cmd.Context(), installCfg)
},
}
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
})

uninstallCmd := &cobra.Command{
Use: "uninstall",
Expand Down Expand Up @@ -434,8 +444,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)
}
Expand Down
112 changes: 106 additions & 6 deletions go/cli/internal/cli/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,27 @@ import (
"fmt"
"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"
"github.com/briandowns/spinner"
"github.com/kagent-dev/kagent/go/cli/internal/config"
"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, 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",
Expand All @@ -41,13 +51,21 @@ func installChart(ctx context.Context, chartName string, namespace string, regis
}

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 *InstallCfg) *PortForward {
if version.Version == "dev" {
fmt.Fprintln(os.Stderr, "Installation requires released version of kagent")
return nil
Expand All @@ -56,6 +74,42 @@ 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)

// setup profile if provided
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(cfg.Profile)
}

return install(ctx, cfg.Config, 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)
Expand All @@ -66,6 +120,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.GetProfileYaml(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{
Expand All @@ -82,14 +160,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()

Expand All @@ -108,8 +195,21 @@ 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 "********"
// 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 {
redactedValues = append(redactedValues, value)
Copy link

Copilot AI Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API key redaction logic assumes all values containing 'apiKey' have exactly one '=' separator. If a value has multiple '=' characters or no '=' character, this could cause index out of bounds or incorrect redaction.

Copilot uses AI. Check for mistakes.
}
}

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)
Expand Down
9 changes: 9 additions & 0 deletions go/cli/internal/profiles/README.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 23 additions & 0 deletions go/cli/internal/profiles/demo.yaml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions go/cli/internal/profiles/minimal.yaml
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions go/cli/internal/profiles/profiles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package profiles

import _ "embed"

//go:embed demo.yaml
var DemoProfileYaml string

//go:embed minimal.yaml
var MinimalProfileYaml string

const (
ProfileDemo = "demo"
ProfileMinimal = "minimal"
)

var Profiles = []string{ProfileMinimal, ProfileDemo}

func GetProfileYaml(profile string) string {
switch profile {
case ProfileDemo:
return DemoProfileYaml
case ProfileMinimal:
return MinimalProfileYaml
default:
return DemoProfileYaml
}
}
2 changes: 0 additions & 2 deletions helm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading