diff --git a/go/cli/cmd/kagent/main.go b/go/cli/cmd/kagent/main.go index 210bbbe71..a85f184d3 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,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", @@ -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) } diff --git a/go/cli/internal/cli/install.go b/go/cli/internal/cli/install.go index d48aab5e4..1f58f0d3d 100644 --- a/go/cli/internal/cli/install.go +++ b/go/cli/internal/cli/install.go @@ -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", @@ -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 @@ -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) @@ -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{ @@ -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() @@ -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) + } + } + + 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..3074eee71 --- /dev/null +++ b/go/cli/internal/profiles/profiles.go @@ -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 + } +} 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