diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5b09238ed..4fe90c343 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -214,3 +214,20 @@ jobs: run: make build-${{ matrix.image }} working-directory: ./ + go-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.24" + cache: true + cache-dependency-path: go/go.sum + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.1 + working-directory: go \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index c4e7b5173..e839aad6d 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -58,3 +58,25 @@ docker buildx create --name kagent-builder-v0.23.0 --platform linux/amd64,linux/ ``` Then run the `make helm-install` command again. + +### Run kagent and an agent locally. + +create a minimal cluster with kind. scale kagent to 0 replicas, as we will run it locally. + +```bash +make create-kind-cluster helm-install-provider helm-tools push-test-agent +kubectl scale -n kagent deployment kagent-controller --replicas 0 +``` + +Run kagent with `KAGENT_A2A_DEBUG_ADDR=localhost:8080` environment variable set, and when it connect to agents it will go to "localhost:8080" instead of the Kubernetes service. + +Run the agent locally as well, with `--net=host` option, so it can connect to the kagent service on localhost. For example: + +```bash +docker run --rm \ + -e KAGENT_URL=http://localhost:8083 \ + -e KAGENT_NAME=kebab-agent \ + -e KAGENT_NAMESPACE=kagent \ + --net=host \ + localhost:5001/kebab:latest +``` diff --git a/go/controller/.gitignore b/go/.gitignore similarity index 100% rename from go/controller/.gitignore rename to go/.gitignore diff --git a/go/.golangci.yaml b/go/.golangci.yaml new file mode 100644 index 000000000..b7bc9bf7a --- /dev/null +++ b/go/.golangci.yaml @@ -0,0 +1,11 @@ +version: "2" + +linters: + # Default set of linters. + # The value can be: + # - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default + # - `all`: enables all linters by default. + # - `none`: disables all linters by default. + # - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`). + # Default: standard + default: standard \ No newline at end of file diff --git a/go/Dockerfile b/go/Dockerfile index 349fb49ec..9aa0fc968 100644 --- a/go/Dockerfile +++ b/go/Dockerfile @@ -18,9 +18,10 @@ RUN --mount=type=cache,target=/root/go/pkg/mod,rw \ go mod download # Copy the go source +COPY api api +COPY cmd cmd COPY pkg pkg COPY internal internal -COPY controller controller # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO @@ -30,7 +31,7 @@ ARG LDFLAGS RUN --mount=type=cache,target=/root/go/pkg/mod,rw \ --mount=type=cache,target=/root/.cache/go-build,rw \ echo "Building on $BUILDPLATFORM -> linux/$TARGETARCH" && \ - CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -ldflags "$LDFLAGS" -o manager controller/cmd/main.go + CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -ldflags "$LDFLAGS" -o manager cmd/controller/main.go ### STAGE 2: final image # Use distroless as minimal base image to package the manager binary diff --git a/go/Makefile b/go/Makefile index 94f700986..da57cf5ce 100644 --- a/go/Makefile +++ b/go/Makefile @@ -16,12 +16,12 @@ endif ##@ Development .PHONY: manifests -manifests: controller-gen generate ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases +manifests: controller-gen generate ## Generate ClusterRole and CustomResourceDefinition objects. + $(CONTROLLER_GEN) rbac:roleName=manager-role crd paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - $(CONTROLLER_GEN) object:headerFile="controller/hack/boilerplate.go.txt" paths="./..." + $(CONTROLLER_GEN) object:headerFile="internal/controller/hack/boilerplate.go.txt" paths="./..." .PHONY: fmt fmt: ## Run go fmt against code. @@ -94,7 +94,7 @@ build: bin/kagent-linux-amd64.sha256 bin/kagent-linux-arm64.sha256 bin/kagent-da .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run ./controller/cmd/main.go + go run ./cmd/controller/main.go .PHONY: test test: @@ -134,7 +134,7 @@ CONTROLLER_TOOLS_VERSION ?= v0.17.1 ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') -GOLANGCI_LINT_VERSION ?= v1.63.4 +GOLANGCI_LINT_VERSION ?= v2.4.0 .PHONY: controller-gen controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. @@ -157,7 +157,7 @@ $(ENVTEST): $(LOCALBIN) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) - $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary diff --git a/go/controller/api/v1alpha1/agent_types.go b/go/api/v1alpha1/agent_types.go similarity index 100% rename from go/controller/api/v1alpha1/agent_types.go rename to go/api/v1alpha1/agent_types.go diff --git a/go/controller/api/v1alpha1/groupversion_info.go b/go/api/v1alpha1/groupversion_info.go similarity index 100% rename from go/controller/api/v1alpha1/groupversion_info.go rename to go/api/v1alpha1/groupversion_info.go diff --git a/go/controller/api/v1alpha1/memory_types.go b/go/api/v1alpha1/memory_types.go similarity index 100% rename from go/controller/api/v1alpha1/memory_types.go rename to go/api/v1alpha1/memory_types.go diff --git a/go/controller/api/v1alpha1/modelconfig_types.go b/go/api/v1alpha1/modelconfig_types.go similarity index 100% rename from go/controller/api/v1alpha1/modelconfig_types.go rename to go/api/v1alpha1/modelconfig_types.go diff --git a/go/controller/api/v1alpha1/toolserver_types.go b/go/api/v1alpha1/toolserver_types.go similarity index 100% rename from go/controller/api/v1alpha1/toolserver_types.go rename to go/api/v1alpha1/toolserver_types.go diff --git a/go/controller/api/v1alpha1/zz_generated.deepcopy.go b/go/api/v1alpha1/zz_generated.deepcopy.go similarity index 100% rename from go/controller/api/v1alpha1/zz_generated.deepcopy.go rename to go/api/v1alpha1/zz_generated.deepcopy.go diff --git a/go/controller/api/v1alpha2/agent_types.go b/go/api/v1alpha2/agent_types.go similarity index 100% rename from go/controller/api/v1alpha2/agent_types.go rename to go/api/v1alpha2/agent_types.go diff --git a/go/controller/api/v1alpha2/groupversion_info.go b/go/api/v1alpha2/groupversion_info.go similarity index 100% rename from go/controller/api/v1alpha2/groupversion_info.go rename to go/api/v1alpha2/groupversion_info.go diff --git a/go/controller/api/v1alpha2/modelconfig_types.go b/go/api/v1alpha2/modelconfig_types.go similarity index 100% rename from go/controller/api/v1alpha2/modelconfig_types.go rename to go/api/v1alpha2/modelconfig_types.go diff --git a/go/controller/api/v1alpha2/remotemcpserver_types.go b/go/api/v1alpha2/remotemcpserver_types.go similarity index 100% rename from go/controller/api/v1alpha2/remotemcpserver_types.go rename to go/api/v1alpha2/remotemcpserver_types.go diff --git a/go/controller/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go similarity index 100% rename from go/controller/api/v1alpha2/zz_generated.deepcopy.go rename to go/api/v1alpha2/zz_generated.deepcopy.go diff --git a/go/cli/cmd/kagent/main.go b/go/cli/cmd/kagent/main.go index a4478d830..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", @@ -70,7 +76,7 @@ func main() { invokeCmd.Flags().BoolVarP(&invokeCfg.Stream, "stream", "S", false, "Stream the response") invokeCmd.Flags().StringVarP(&invokeCfg.File, "file", "f", "", "File to read the task from") invokeCmd.Flags().StringVarP(&invokeCfg.URLOverride, "url-override", "u", "", "URL override") - invokeCmd.Flags().MarkHidden("url-override") + invokeCmd.Flags().MarkHidden("url-override") //nolint:errcheck bugReportCmd := &cobra.Command{ Use: "bug-report", @@ -123,7 +129,7 @@ func main() { Long: `Get a kagent resource`, Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(os.Stderr, "No resource type provided\n\n") - cmd.Help() + cmd.Help() //nolint:errcheck os.Exit(1) }, } @@ -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/const.go b/go/cli/internal/cli/const.go index a27fa95c0..30a5e7a20 100644 --- a/go/cli/internal/cli/const.go +++ b/go/cli/internal/cli/const.go @@ -4,7 +4,7 @@ import ( "os" "strings" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha1" ) const ( diff --git a/go/cli/internal/cli/const_test.go b/go/cli/internal/cli/const_test.go index 4a19b69f4..ebe7dba62 100644 --- a/go/cli/internal/cli/const_test.go +++ b/go/cli/internal/cli/const_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha1" ) func TestGetModelProvider(t *testing.T) { @@ -62,10 +62,10 @@ func TestGetModelProvider(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if tc.envVarValue == "" { - os.Unsetenv(KAGENT_DEFAULT_MODEL_PROVIDER) + os.Unsetenv(KAGENT_DEFAULT_MODEL_PROVIDER) //nolint:errcheck } else { - os.Setenv(KAGENT_DEFAULT_MODEL_PROVIDER, tc.expectedHelmKey) - defer os.Unsetenv(KAGENT_DEFAULT_MODEL_PROVIDER) + os.Setenv(KAGENT_DEFAULT_MODEL_PROVIDER, tc.expectedHelmKey) //nolint:errcheck + defer os.Unsetenv(KAGENT_DEFAULT_MODEL_PROVIDER) //nolint:errcheck } result := GetModelProvider() diff --git a/go/cli/internal/cli/get.go b/go/cli/internal/cli/get.go index 54bcec421..112ee24f2 100644 --- a/go/cli/internal/cli/get.go +++ b/go/cli/internal/cli/get.go @@ -41,7 +41,7 @@ func GetAgentCmd(cfg *config.Config, resourceName string) { return } byt, _ := json.MarshalIndent(agent, "", " ") - fmt.Fprintln(os.Stdout, string(byt)) + fmt.Fprintln(os.Stdout, string(byt)) //nolint:errcheck } } @@ -70,7 +70,7 @@ func GetSessionCmd(cfg *config.Config, resourceName string) { return } byt, _ := json.MarshalIndent(session, "", " ") - fmt.Fprintln(os.Stdout, string(byt)) + fmt.Fprintln(os.Stdout, string(byt)) //nolint:errcheck } } diff --git a/go/cli/internal/cli/install.go b/go/cli/internal/cli/install.go index 1c2acaab8..83408e287 100644 --- a/go/cli/internal/cli/install.go +++ b/go/cli/internal/cli/install.go @@ -8,14 +8,17 @@ import ( "strings" "time" + "github.com/kagent-dev/kagent/go/controller/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" ) // 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,23 +38,70 @@ 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...) + + // 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 + } + + // 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() @@ -65,6 +115,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{ @@ -79,18 +153,25 @@ func InstallCmd(ctx context.Context, cfg *config.Config) *PortForward { // split helmExtraArgs by "--set" to get additional values extraValues := strings.Split(helmExtraArgs, "--set") - for _, hev := range extraValues { - values = append(values, hev) + 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() @@ -109,8 +190,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) @@ -119,7 +212,7 @@ func InstallCmd(ctx context.Context, cfg *config.Config) *PortForward { // Stop the spinner completely before printing the success message s.Stop() - fmt.Fprintln(os.Stdout, "kagent installed successfully") + fmt.Fprintln(os.Stdout, "kagent installed successfully") //nolint:errcheck pf, err := NewPortForward(ctx, cfg) if err != nil { @@ -146,11 +239,11 @@ func deleteCRDs(ctx context.Context) error { if out, err := deleteCmd.CombinedOutput(); err != nil { if !strings.Contains(string(out), "not found") { errMsg := fmt.Sprintf("Error deleting CRD %s: %s", crd, string(out)) - fmt.Fprintln(os.Stderr, errMsg) + fmt.Fprintln(os.Stderr, errMsg) //nolint:errcheck deleteErrors = append(deleteErrors, errMsg) } } else { - fmt.Fprintf(os.Stdout, "Successfully deleted CRD %s\n", crd) + fmt.Fprintf(os.Stdout, "Successfully deleted CRD %s\n", crd) //nolint:errcheck } } @@ -216,5 +309,5 @@ func UninstallCmd(ctx context.Context, cfg *config.Config) { } s.Stop() - fmt.Fprintln(os.Stdout, "\nkagent uninstalled successfully") + fmt.Fprintln(os.Stdout, "\nkagent uninstalled successfully") //nolint:errcheck } diff --git a/go/cli/internal/cli/invoke.go b/go/cli/internal/cli/invoke.go index 461564190..74a335581 100644 --- a/go/cli/internal/cli/invoke.go +++ b/go/cli/internal/cli/invoke.go @@ -134,6 +134,6 @@ func InvokeCmd(ctx context.Context, cfg *InvokeCfg) { return } - fmt.Fprintf(os.Stdout, "%+v\n", string(jsn)) + fmt.Fprintf(os.Stdout, "%+v\n", string(jsn)) //nolint:errcheck } } diff --git a/go/cli/internal/cli/utils.go b/go/cli/internal/cli/utils.go index 7ee90d34f..603bc523d 100644 --- a/go/cli/internal/cli/utils.go +++ b/go/cli/internal/cli/utils.go @@ -13,7 +13,7 @@ import ( ) var ( - ErrServerConnection = fmt.Errorf("Error connecting to server. Please run 'install' command first.") + ErrServerConnection = fmt.Errorf("error connecting to server. Please run 'install' command first") ) func CheckServerConnection(client *client.ClientSet) error { @@ -70,7 +70,7 @@ func (p *PortForward) Stop() { p.cancel() // This will terminate the kubectl process in case the cancel does not work. if p.cmd.Process != nil { - p.cmd.Process.Kill() + p.cmd.Process.Kill() //nolint:errcheck } // Don't wait for the process - just cancel the context and let it die @@ -85,15 +85,15 @@ func StreamA2AEvents(ch <-chan protocol.StreamingMessageEvent, verbose bool) { fmt.Fprintf(os.Stderr, "Error marshaling A2A event: %v\n", err) continue } - fmt.Fprintf(os.Stdout, "%+v\n", string(json)) + fmt.Fprintf(os.Stdout, "%+v\n", string(json)) //nolint:errcheck } else { json, err := event.MarshalJSON() if err != nil { fmt.Fprintf(os.Stderr, "Error marshaling A2A event: %v\n", err) continue } - fmt.Fprintf(os.Stdout, "%+v\n", string(json)) + fmt.Fprintf(os.Stdout, "%+v\n", string(json)) //nolint:errcheck } } - fmt.Fprintln(os.Stdout) // Add a newline after streaming is complete + fmt.Fprintln(os.Stdout) //nolint:errcheck // Add a newline after streaming is complete } diff --git a/go/cli/internal/cli/version.go b/go/cli/internal/cli/version.go index e76da8e7e..4d699efdd 100644 --- a/go/cli/internal/cli/version.go +++ b/go/cli/internal/cli/version.go @@ -28,5 +28,5 @@ func VersionCmd(cfg *config.Config) { versionInfo["backend_version"] = version.KAgentVersion } - json.NewEncoder(os.Stdout).Encode(versionInfo) + json.NewEncoder(os.Stdout).Encode(versionInfo) //nolint:errcheck } 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/go/cmd/controller/main.go b/go/cmd/controller/main.go new file mode 100644 index 000000000..ad84cbe8f --- /dev/null +++ b/go/cmd/controller/main.go @@ -0,0 +1,33 @@ +/* +Copyright 2025. + +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. +*/ + +package main + +import ( + "github.com/kagent-dev/kagent/go/internal/httpserver/auth" + "github.com/kagent-dev/kagent/go/pkg/app" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" +) + +// nolint:gocyclo +func main() { + authorizer := &auth.NoopAuthorizer{} + authenticator := &auth.UnsecureAuthenticator{} + app.Start(authenticator, authorizer) +} diff --git a/go/controller/.dockerignore b/go/controller/.dockerignore deleted file mode 100644 index a3aab7af7..000000000 --- a/go/controller/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file -# Ignore build and test binaries. -bin/ diff --git a/go/controller/README.md b/go/controller/README.md deleted file mode 100644 index 49988809f..000000000 --- a/go/controller/README.md +++ /dev/null @@ -1,134 +0,0 @@ -# controller -// TODO(user): Add simple overview of use/purpose - -## Description -// TODO(user): An in-depth paragraph about your project and overview of use - -## Getting Started - -### Prerequisites -- go version v1.24.0+ -- docker version 17.03+. -- kubectl version v1.11.3+. -- Access to a Kubernetes v1.11.3+ cluster. - -### To Deploy on the cluster -**Build and push your image to the location specified by `IMG`:** - -```sh -make docker-build docker-push IMG=/controller:tag -``` - -**NOTE:** This image ought to be published in the personal registry you specified. -And it is required to have access to pull the image from the working environment. -Make sure you have the proper permission to the registry if the above commands don’t work. - -**Install the CRDs into the cluster:** - -```sh -make install -``` - -**Deploy the Manager to the cluster with the image specified by `IMG`:** - -```sh -make deploy IMG=/controller:tag -``` - -> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin -privileges or be logged in as admin. - -**Create instances of your solution** -You can apply the samples (examples) from the config/sample: - -```sh -kubectl apply -k config/samples/ -``` - ->**NOTE**: Ensure that the samples has default values to test it out. - -### To Uninstall -**Delete the instances (CRs) from the cluster:** - -```sh -kubectl delete -k config/samples/ -``` - -**Delete the APIs(CRDs) from the cluster:** - -```sh -make uninstall -``` - -**UnDeploy the controller from the cluster:** - -```sh -make undeploy -``` - -## Project Distribution - -Following the options to release and provide this solution to the users. - -### By providing a bundle with all YAML files - -1. Build the installer for the image built and published in the registry: - -```sh -make build-installer IMG=/controller:tag -``` - -**NOTE:** The makefile target mentioned above generates an 'install.yaml' -file in the dist directory. This file contains all the resources built -with Kustomize, which are necessary to install this project without its -dependencies. - -2. Using the installer - -Users can just run 'kubectl apply -f ' to install -the project, i.e.: - -```sh -kubectl apply -f https://raw.githubusercontent.com//controller//dist/install.yaml -``` - -### By providing a Helm Chart - -1. Build the chart using the optional helm plugin - -```sh -kubebuilder edit --plugins=helm/v1-alpha -``` - -2. See that a chart was generated under 'dist/chart', and users -can obtain this solution from there. - -**NOTE:** If you change the project, you need to update the Helm Chart -using the same command above to sync the latest changes. Furthermore, -if you create webhooks, you need to use the above command with -the '--force' flag and manually ensure that any custom configuration -previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' -is manually re-applied afterwards. - -## Contributing -// TODO(user): Add detailed information on how you would like others to contribute to this project - -**NOTE:** Run `make help` for more information on all potential `make` targets - -More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) - -## License - -Copyright 2025. - -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. diff --git a/go/internal/a2a/a2a_handler_mux.go b/go/internal/a2a/a2a_handler_mux.go index b1752701e..c4f9b09ab 100644 --- a/go/internal/a2a/a2a_handler_mux.go +++ b/go/internal/a2a/a2a_handler_mux.go @@ -84,7 +84,7 @@ func (a *handlerMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "Agent namespace not provided", http.StatusBadRequest) return } - agentName, remainingPath := popPath(remainingPath) + agentName, _ := popPath(remainingPath) if agentName == "" { http.Error(w, "Agent name not provided", http.StatusBadRequest) return diff --git a/go/controller/internal/a2a/a2a_reconciler.go b/go/internal/controller/a2a/a2a_reconciler.go similarity index 54% rename from go/controller/internal/a2a/a2a_reconciler.go rename to go/internal/controller/a2a/a2a_reconciler.go index 9c7d7b218..f9f693f9c 100644 --- a/go/controller/internal/a2a/a2a_reconciler.go +++ b/go/internal/controller/a2a/a2a_reconciler.go @@ -3,20 +3,19 @@ package a2a import ( "context" "fmt" + "net" + "net/http" + "os" + "time" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/a2a" "github.com/kagent-dev/kagent/go/internal/httpserver/auth" common "github.com/kagent-dev/kagent/go/internal/utils" - ctrl "sigs.k8s.io/controller-runtime" a2aclient "trpc.group/trpc-go/trpc-a2a-go/client" "trpc.group/trpc-go/trpc-a2a-go/server" ) -var ( - reconcileLog = ctrl.Log.WithName("a2a_reconcile") -) - type A2AReconciler interface { ReconcileAgent( ctx context.Context, @@ -29,28 +28,30 @@ type A2AReconciler interface { ) } -type a2aReconciler struct { - a2aHandler a2a.A2AHandlerMux - a2aBaseUrl string +type ClientOptions struct { + StreamingMaxBufSize int + StreamingInitialBufSize int + Timeout time.Duration +} - streamingMaxBufSize int - streamingInitialBufSize int - authenticator auth.AuthProvider +type a2aReconciler struct { + a2aHandler a2a.A2AHandlerMux + a2aBaseUrl string + authenticator auth.AuthProvider + clientOptions ClientOptions } func NewReconciler( a2aHandler a2a.A2AHandlerMux, a2aBaseUrl string, - streamingMaxBufSize int, - streamingInitialBufSize int, + clientOptions ClientOptions, authenticator auth.AuthProvider, ) A2AReconciler { return &a2aReconciler{ - a2aHandler: a2aHandler, - a2aBaseUrl: a2aBaseUrl, - streamingMaxBufSize: streamingMaxBufSize, - streamingInitialBufSize: streamingInitialBufSize, - authenticator: authenticator, + a2aHandler: a2aHandler, + a2aBaseUrl: a2aBaseUrl, + clientOptions: clientOptions, + authenticator: authenticator, } } @@ -62,7 +63,9 @@ func (a *a2aReconciler) ReconcileAgent( agentRef := common.GetObjectRef(agent) client, err := a2aclient.NewA2AClient(card.URL, - a2aclient.WithBuffer(a.streamingInitialBufSize, a.streamingMaxBufSize), + a2aclient.WithTimeout(a.clientOptions.Timeout), + a2aclient.WithBuffer(a.clientOptions.StreamingInitialBufSize, a.clientOptions.StreamingMaxBufSize), + debugOpt(), a2aclient.WithHTTPReqHandler(auth.A2ARequestHandler(a.authenticator)), ) if err != nil { @@ -85,3 +88,19 @@ func (a *a2aReconciler) ReconcileAgentDeletion( ) { a.a2aHandler.RemoveAgentHandler(agentRef) } + +func debugOpt() a2aclient.Option { + debugAddr := os.Getenv("KAGENT_A2A_DEBUG_ADDR") + if debugAddr != "" { + client := new(http.Client) + client.Transport = &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + var zeroDialer net.Dialer + return zeroDialer.DialContext(ctx, network, debugAddr) + }, + } + return a2aclient.WithHTTPClient(client) + } else { + return func(*a2aclient.A2AClient) {} + } +} diff --git a/go/controller/internal/controller/agent_controller.go b/go/internal/controller/agent_controller.go similarity index 98% rename from go/controller/internal/controller/agent_controller.go rename to go/internal/controller/agent_controller.go index 6be7dc2bd..d1ded2162 100644 --- a/go/controller/internal/controller/agent_controller.go +++ b/go/internal/controller/agent_controller.go @@ -35,8 +35,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" - "github.com/kagent-dev/kagent/go/controller/internal/reconciler" + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/internal/controller/reconciler" v1alpha1 "github.com/kagent-dev/kmcp/api/v1alpha1" ) diff --git a/go/controller/hack/boilerplate.go.txt b/go/internal/controller/hack/boilerplate.go.txt similarity index 100% rename from go/controller/hack/boilerplate.go.txt rename to go/internal/controller/hack/boilerplate.go.txt diff --git a/go/controller/internal/controller/mcp_server_controller.go b/go/internal/controller/mcp_server_controller.go similarity index 96% rename from go/controller/internal/controller/mcp_server_controller.go rename to go/internal/controller/mcp_server_controller.go index c2a43095d..2f543383a 100644 --- a/go/controller/internal/controller/mcp_server_controller.go +++ b/go/internal/controller/mcp_server_controller.go @@ -20,7 +20,7 @@ import ( "context" "time" - "github.com/kagent-dev/kagent/go/controller/internal/reconciler" + "github.com/kagent-dev/kagent/go/internal/controller/reconciler" "github.com/kagent-dev/kmcp/api/v1alpha1" "k8s.io/apimachinery/pkg/runtime" diff --git a/go/controller/internal/controller/memory_controller.go b/go/internal/controller/memory_controller.go similarity index 93% rename from go/controller/internal/controller/memory_controller.go rename to go/internal/controller/memory_controller.go index 8946c987e..bed355bf5 100644 --- a/go/controller/internal/controller/memory_controller.go +++ b/go/internal/controller/memory_controller.go @@ -19,7 +19,7 @@ package controller import ( "context" - "github.com/kagent-dev/kagent/go/controller/internal/reconciler" + "github.com/kagent-dev/kagent/go/internal/controller/reconciler" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" @@ -29,7 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - agentv1alpha1 "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + agentv1alpha1 "github.com/kagent-dev/kagent/go/api/v1alpha1" ) // MemoryController reconciles a Memory object diff --git a/go/controller/internal/controller/modelconfig_controller.go b/go/internal/controller/modelconfig_controller.go similarity index 96% rename from go/controller/internal/controller/modelconfig_controller.go rename to go/internal/controller/modelconfig_controller.go index 58209ca6b..fc9d29039 100644 --- a/go/controller/internal/controller/modelconfig_controller.go +++ b/go/internal/controller/modelconfig_controller.go @@ -19,7 +19,7 @@ package controller import ( "context" - "github.com/kagent-dev/kagent/go/controller/internal/reconciler" + "github.com/kagent-dev/kagent/go/internal/controller/reconciler" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -34,7 +34,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" ) var ( diff --git a/go/controller/internal/reconciler/reconciler.go b/go/internal/controller/reconciler/reconciler.go similarity index 92% rename from go/controller/internal/reconciler/reconciler.go rename to go/internal/controller/reconciler/reconciler.go index ce138b535..134b837b7 100644 --- a/go/controller/internal/reconciler/reconciler.go +++ b/go/internal/controller/reconciler/reconciler.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/go-multierror" appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" k8s_errors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,9 +18,9 @@ import ( "k8s.io/utils/ptr" "trpc.group/trpc-go/trpc-a2a-go/server" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" - "github.com/kagent-dev/kagent/go/controller/internal/a2a" - "github.com/kagent-dev/kagent/go/controller/translator" + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/internal/controller/a2a" + "github.com/kagent-dev/kagent/go/internal/controller/translator" "github.com/kagent-dev/kagent/go/internal/database" "github.com/kagent-dev/kagent/go/internal/utils" "github.com/kagent-dev/kagent/go/internal/version" @@ -93,11 +92,11 @@ func (a *kagentReconciler) ReconcileKagentAgent(ctx context.Context, req ctrl.Re func (a *kagentReconciler) handleAgentDeletion(req ctrl.Request) error { // remove a2a handler if it exists - a.a2aReconciler.ReconcileAgentDeletion(req.NamespacedName.String()) + a.a2aReconciler.ReconcileAgentDeletion(req.String()) - if err := a.dbClient.DeleteAgent(req.NamespacedName.String()); err != nil { + if err := a.dbClient.DeleteAgent(req.String()); err != nil { return fmt.Errorf("failed to delete agent %s: %w", - req.NamespacedName.String(), err) + req.String(), err) } reconcileLog.Info("Agent was deleted", "namespace", req.Namespace, "name", req.Name) @@ -147,15 +146,15 @@ func (a *kagentReconciler) reconcileAgentStatus(ctx context.Context, agent *v1al conditionChanged := meta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{ Type: v1alpha2.AgentConditionTypeAccepted, Status: status, - LastTransitionTime: metav1.Now(), Reason: reason, Message: message, + ObservedGeneration: agent.Generation, }) deployedCondition := metav1.Condition{ Type: v1alpha2.AgentConditionTypeReady, Status: metav1.ConditionUnknown, - LastTransitionTime: metav1.Now(), + ObservedGeneration: agent.Generation, } // Check if the deployment exists @@ -180,7 +179,7 @@ func (a *kagentReconciler) reconcileAgentStatus(ctx context.Context, agent *v1al } } - conditionChanged = meta.SetStatusCondition(&agent.Status.Conditions, deployedCondition) + conditionChanged = conditionChanged || meta.SetStatusCondition(&agent.Status.Conditions, deployedCondition) // Only update the config hash if the config hash has changed and there was no error configHashChanged := len(configHash) > 0 && !bytes.Equal((agent.Status.ConfigHash)[:], configHash[:]) @@ -205,15 +204,15 @@ func (a *kagentReconciler) ReconcileKagentMCPService(ctx context.Context, req ct if k8s_errors.IsNotFound(err) { // Delete from DB if the service is deleted dbService := &database.ToolServer{ - Name: req.NamespacedName.String(), + Name: req.String(), GroupKind: schema.GroupKind{Group: "", Kind: "Service"}.String(), } if err := a.dbClient.DeleteToolServer(dbService.Name, dbService.GroupKind); err != nil { - reconcileLog.Error(err, "failed to delete tool server for mcp service", "service", req.NamespacedName.String()) + reconcileLog.Error(err, "failed to delete tool server for mcp service", "service", req.String()) } - reconcileLog.Info("mcp service was deleted", "service", req.NamespacedName.String()) + reconcileLog.Info("mcp service was deleted", "service", req.String()) if err := a.dbClient.DeleteToolsForServer(dbService.Name, dbService.GroupKind); err != nil { - reconcileLog.Error(err, "failed to delete tools for mcp service", "service", req.NamespacedName.String()) + reconcileLog.Error(err, "failed to delete tools for mcp service", "service", req.String()) } return nil } @@ -248,8 +247,8 @@ func (a *kagentReconciler) ReconcileKagentModelConfig(ctx context.Context, req c var err error if modelConfig.Spec.APIKeySecret != "" { - secret := &v1.Secret{} - if err := a.kube.Get(ctx, types.NamespacedName{Namespace: modelConfig.Namespace, Name: modelConfig.Spec.APIKeySecret}, secret); err != nil { + secret := &corev1.Secret{} + if err = a.kube.Get(ctx, types.NamespacedName{Namespace: modelConfig.Namespace, Name: modelConfig.Spec.APIKeySecret}, secret); err != nil { err = fmt.Errorf("failed to get secret %s: %v", modelConfig.Spec.APIKeySecret, err) } } @@ -301,15 +300,15 @@ func (a *kagentReconciler) ReconcileKagentMCPServer(ctx context.Context, req ctr if k8s_errors.IsNotFound(err) { // Delete from DB if the mcp server is deleted dbServer := &database.ToolServer{ - Name: req.NamespacedName.String(), + Name: req.String(), GroupKind: schema.GroupKind{Group: "kagent.dev", Kind: "MCPServer"}.String(), } if err := a.dbClient.DeleteToolServer(dbServer.Name, dbServer.GroupKind); err != nil { - reconcileLog.Error(err, "failed to delete tool server for mcp server", "mcpServer", req.NamespacedName.String()) + reconcileLog.Error(err, "failed to delete tool server for mcp server", "mcpServer", req.String()) } - reconcileLog.Info("mcp server was deleted", "mcpServer", req.NamespacedName.String()) + reconcileLog.Info("mcp server was deleted", "mcpServer", req.String()) if err := a.dbClient.DeleteToolsForServer(dbServer.Name, dbServer.GroupKind); err != nil { - reconcileLog.Error(err, "failed to delete tools for mcp server", "mcpServer", req.NamespacedName.String()) + reconcileLog.Error(err, "failed to delete tools for mcp server", "mcpServer", req.String()) } return nil } @@ -340,15 +339,15 @@ func (a *kagentReconciler) ReconcileKagentRemoteMCPServer(ctx context.Context, r if k8s_errors.IsNotFound(err) { // Delete from DB if the remote mcp server is deleted dbServer := &database.ToolServer{ - Name: req.NamespacedName.String(), + Name: req.String(), GroupKind: schema.GroupKind{Group: "kagent.dev", Kind: "RemoteMCPServer"}.String(), } if err := a.dbClient.DeleteToolServer(dbServer.Name, dbServer.GroupKind); err != nil { - reconcileLog.Error(err, "failed to delete tool server for remote mcp server", "remoteMCPServer", req.NamespacedName.String()) + reconcileLog.Error(err, "failed to delete tool server for remote mcp server", "remoteMCPServer", req.String()) } - reconcileLog.Info("remote mcp server was deleted", "remoteMCPServer", req.NamespacedName.String()) + reconcileLog.Info("remote mcp server was deleted", "remoteMCPServer", req.String()) if err := a.dbClient.DeleteToolsForServer(dbServer.Name, dbServer.GroupKind); err != nil { - reconcileLog.Error(err, "failed to delete tools for remote mcp server", "remoteMCPServer", req.NamespacedName.String()) + reconcileLog.Error(err, "failed to delete tools for remote mcp server", "remoteMCPServer", req.String()) } return nil } @@ -394,18 +393,18 @@ func (a *kagentReconciler) reconcileRemoteMCPServerStatus( if err != nil { status = metav1.ConditionFalse message = err.Error() - reason = "AgentReconcileFailed" + reason = "ReconcileFailed" reconcileLog.Error(err, "failed to reconcile agent", "tool_server", utils.GetObjectRef(toolServer)) } else { status = metav1.ConditionTrue - reason = "AgentReconciled" + reason = "Reconciled" } conditionChanged := meta.SetStatusCondition(&toolServer.Status.Conditions, metav1.Condition{ Type: v1alpha2.AgentConditionTypeAccepted, Status: status, - LastTransitionTime: metav1.Now(), Reason: reason, Message: message, + ObservedGeneration: toolServer.Generation, }) // only update if the status has changed to prevent looping the reconciler @@ -419,7 +418,7 @@ func (a *kagentReconciler) reconcileRemoteMCPServerStatus( toolServer.Status.DiscoveredTools = discoveredTools if err := a.kube.Status().Update(ctx, toolServer); err != nil { - return fmt.Errorf("failed to update agent status: %v", err) + return fmt.Errorf("failed to update remote mcp server status: %v", err) } return nil @@ -513,8 +512,8 @@ func (a *kagentReconciler) createMcpTransport(ctx context.Context, s *v1alpha2.R return nil, err } - switch { - case s.Protocol == v1alpha2.RemoteMCPServerProtocolSse: + switch s.Protocol { + case v1alpha2.RemoteMCPServerProtocolSse: return transport.NewSSE(s.URL, transport.WithHeaders(headers)) default: return transport.NewStreamableHTTP(s.URL, transport.WithHTTPHeaders(headers)) @@ -527,7 +526,7 @@ func (a *kagentReconciler) listTools(ctx context.Context, tsp transport.Interfac if err != nil { return nil, fmt.Errorf("failed to start client for toolServer %s: %v", toolServer.Name, err) } - defer client.Close() + defer client.Close() //nolint:errcheck _, err = client.Initialize(ctx, mcp.InitializeRequest{ Params: mcp.InitializeParams{ ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, diff --git a/go/controller/internal/controller/remote_mcp_server_controller.go b/go/internal/controller/remote_mcp_server_controller.go similarity index 93% rename from go/controller/internal/controller/remote_mcp_server_controller.go rename to go/internal/controller/remote_mcp_server_controller.go index 0a4a8f235..fe43b48e6 100644 --- a/go/controller/internal/controller/remote_mcp_server_controller.go +++ b/go/internal/controller/remote_mcp_server_controller.go @@ -20,8 +20,8 @@ import ( "context" "time" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" - "github.com/kagent-dev/kagent/go/controller/internal/reconciler" + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/internal/controller/reconciler" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" diff --git a/go/controller/internal/controller/service_controller.go b/go/internal/controller/service_controller.go similarity index 96% rename from go/controller/internal/controller/service_controller.go rename to go/internal/controller/service_controller.go index ea4ec892c..5ec9b03aa 100644 --- a/go/controller/internal/controller/service_controller.go +++ b/go/internal/controller/service_controller.go @@ -19,7 +19,7 @@ package controller import ( "context" - "github.com/kagent-dev/kagent/go/controller/internal/reconciler" + "github.com/kagent-dev/kagent/go/internal/controller/reconciler" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/go/controller/translator/adk_api_translator.go b/go/internal/controller/translator/adk_api_translator.go similarity index 97% rename from go/controller/translator/adk_api_translator.go rename to go/internal/controller/translator/adk_api_translator.go index 26b06d6e5..2f41db807 100644 --- a/go/controller/translator/adk_api_translator.go +++ b/go/internal/controller/translator/adk_api_translator.go @@ -12,10 +12,9 @@ import ( "strconv" "strings" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/adk" "github.com/kagent-dev/kagent/go/internal/utils" - common "github.com/kagent-dev/kagent/go/internal/utils" "github.com/kagent-dev/kagent/go/internal/version" "github.com/kagent-dev/kmcp/api/v1alpha1" appsv1 "k8s.io/api/apps/v1" @@ -28,7 +27,6 @@ import ( "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "trpc.group/trpc-go/trpc-a2a-go/server" ) @@ -63,8 +61,6 @@ type AgentOutputs struct { AgentCard server.AgentCard `json:"agentCard"` } -var adkLog = ctrllog.Log.WithName("adk") - type AdkApiTranslator interface { TranslateAgent( ctx context.Context, @@ -82,7 +78,6 @@ func NewAdkApiTranslator(kube client.Client, defaultModelConfig types.Namespaced type adkApiTranslator struct { kube client.Client defaultModelConfig types.NamespacedName - imageConfig ImageConfig } const MAX_DEPTH = 10 @@ -98,7 +93,7 @@ type tState struct { func (s *tState) with(agent *v1alpha2.Agent) *tState { s.depth++ - s.visitedAgents = append(s.visitedAgents, common.GetObjectRef(agent)) + s.visitedAgents = append(s.visitedAgents, utils.GetObjectRef(agent)) return s } @@ -199,7 +194,7 @@ func (a *adkApiTranslator) buildManifest( }, corev1.EnvVar{ Name: "KAGENT_URL", - Value: fmt.Sprintf("http://kagent-controller.%s:8083", common.GetResourceNamespace()), + Value: fmt.Sprintf("http://kagent-controller.%s:8083", utils.GetResourceNamespace()), }, ) @@ -402,9 +397,7 @@ func (a *adkApiTranslator) translateInlineAgent(ctx context.Context, agent *v1al // Skip tools that are not applicable to the model provider switch { case tool.McpServer != nil: - for _, toolName := range tool.McpServer.ToolNames { - toolsByServer[tool.McpServer.TypedLocalReference] = append(toolsByServer[tool.McpServer.TypedLocalReference], toolName) - } + toolsByServer[tool.McpServer.TypedLocalReference] = append(toolsByServer[tool.McpServer.TypedLocalReference], tool.McpServer.ToolNames...) case tool.Agent != nil: agentRef := types.NamespacedName{ @@ -647,7 +640,7 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC return anthropic, modelDeploymentData, nil case v1alpha2.ModelProviderOllama: if model.Spec.Ollama == nil { - return nil, nil, fmt.Errorf("Ollama model config is required") + return nil, nil, fmt.Errorf("ollama model config is required") } modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{ Name: "OLLAMA_API_BASE", @@ -839,7 +832,7 @@ func ConvertServiceToRemoteMCPServer(svc *corev1.Service) (*v1alpha2.RemoteMCPSe func ConvertMCPServerToRemoteMCPServer(mcpServer *v1alpha1.MCPServer) (*v1alpha2.RemoteMCPServerSpec, error) { if mcpServer.Spec.Deployment.Port == 0 { - return nil, fmt.Errorf("Cannot determine port for MCP server %s", mcpServer.Name) + return nil, fmt.Errorf("cannot determine port for MCP server %s", mcpServer.Name) } return &v1alpha2.RemoteMCPServerSpec{ @@ -848,8 +841,8 @@ func ConvertMCPServerToRemoteMCPServer(mcpServer *v1alpha1.MCPServer) (*v1alpha2 }, nil } func (a *adkApiTranslator) translateRemoteMCPServerTarget(ctx context.Context, agent *adk.AgentConfig, remoteMcpServer *v1alpha2.RemoteMCPServerSpec, toolNames []string, agentNamespace string) error { - switch { - case remoteMcpServer.Protocol == v1alpha2.RemoteMCPServerProtocolSse: + switch remoteMcpServer.Protocol { + case v1alpha2.RemoteMCPServerProtocolSse: tool, err := a.translateSseHttpTool(ctx, remoteMcpServer, agentNamespace) if err != nil { return err diff --git a/go/controller/translator/adk_translator_golden_test.go b/go/internal/controller/translator/adk_translator_golden_test.go similarity index 98% rename from go/controller/translator/adk_translator_golden_test.go rename to go/internal/controller/translator/adk_translator_golden_test.go index d40ee2fc8..4af037fd8 100644 --- a/go/controller/translator/adk_translator_golden_test.go +++ b/go/internal/controller/translator/adk_translator_golden_test.go @@ -12,8 +12,8 @@ import ( "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" - "github.com/kagent-dev/kagent/go/controller/translator" + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/internal/controller/translator" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" diff --git a/go/controller/translator/testdata/README.md b/go/internal/controller/translator/testdata/README.md similarity index 100% rename from go/controller/translator/testdata/README.md rename to go/internal/controller/translator/testdata/README.md diff --git a/go/controller/translator/testdata/inputs/agent_with_http_toolserver.yaml b/go/internal/controller/translator/testdata/inputs/agent_with_http_toolserver.yaml similarity index 100% rename from go/controller/translator/testdata/inputs/agent_with_http_toolserver.yaml rename to go/internal/controller/translator/testdata/inputs/agent_with_http_toolserver.yaml diff --git a/go/controller/translator/testdata/inputs/agent_with_mcp_service.yaml b/go/internal/controller/translator/testdata/inputs/agent_with_mcp_service.yaml similarity index 100% rename from go/controller/translator/testdata/inputs/agent_with_mcp_service.yaml rename to go/internal/controller/translator/testdata/inputs/agent_with_mcp_service.yaml diff --git a/go/controller/translator/testdata/inputs/agent_with_nested_agent.yaml b/go/internal/controller/translator/testdata/inputs/agent_with_nested_agent.yaml similarity index 100% rename from go/controller/translator/testdata/inputs/agent_with_nested_agent.yaml rename to go/internal/controller/translator/testdata/inputs/agent_with_nested_agent.yaml diff --git a/go/controller/translator/testdata/inputs/anthropic_agent.yaml b/go/internal/controller/translator/testdata/inputs/anthropic_agent.yaml similarity index 100% rename from go/controller/translator/testdata/inputs/anthropic_agent.yaml rename to go/internal/controller/translator/testdata/inputs/anthropic_agent.yaml diff --git a/go/controller/translator/testdata/inputs/basic_agent.yaml b/go/internal/controller/translator/testdata/inputs/basic_agent.yaml similarity index 100% rename from go/controller/translator/testdata/inputs/basic_agent.yaml rename to go/internal/controller/translator/testdata/inputs/basic_agent.yaml diff --git a/go/controller/translator/testdata/inputs/ollama_agent.yaml b/go/internal/controller/translator/testdata/inputs/ollama_agent.yaml similarity index 100% rename from go/controller/translator/testdata/inputs/ollama_agent.yaml rename to go/internal/controller/translator/testdata/inputs/ollama_agent.yaml diff --git a/go/controller/translator/testdata/outputs/agent_with_http_toolserver.json b/go/internal/controller/translator/testdata/outputs/agent_with_http_toolserver.json similarity index 100% rename from go/controller/translator/testdata/outputs/agent_with_http_toolserver.json rename to go/internal/controller/translator/testdata/outputs/agent_with_http_toolserver.json diff --git a/go/controller/translator/testdata/outputs/agent_with_mcp_service.json b/go/internal/controller/translator/testdata/outputs/agent_with_mcp_service.json similarity index 100% rename from go/controller/translator/testdata/outputs/agent_with_mcp_service.json rename to go/internal/controller/translator/testdata/outputs/agent_with_mcp_service.json diff --git a/go/controller/translator/testdata/outputs/agent_with_nested_agent.json b/go/internal/controller/translator/testdata/outputs/agent_with_nested_agent.json similarity index 100% rename from go/controller/translator/testdata/outputs/agent_with_nested_agent.json rename to go/internal/controller/translator/testdata/outputs/agent_with_nested_agent.json diff --git a/go/controller/translator/testdata/outputs/anthropic_agent.json b/go/internal/controller/translator/testdata/outputs/anthropic_agent.json similarity index 100% rename from go/controller/translator/testdata/outputs/anthropic_agent.json rename to go/internal/controller/translator/testdata/outputs/anthropic_agent.json diff --git a/go/controller/translator/testdata/outputs/basic_agent.json b/go/internal/controller/translator/testdata/outputs/basic_agent.json similarity index 100% rename from go/controller/translator/testdata/outputs/basic_agent.json rename to go/internal/controller/translator/testdata/outputs/basic_agent.json diff --git a/go/controller/translator/testdata/outputs/ollama_agent.json b/go/internal/controller/translator/testdata/outputs/ollama_agent.json similarity index 100% rename from go/controller/translator/testdata/outputs/ollama_agent.json rename to go/internal/controller/translator/testdata/outputs/ollama_agent.json diff --git a/go/internal/database/client.go b/go/internal/database/client.go index d6eb84d80..50ab045e2 100644 --- a/go/internal/database/client.go +++ b/go/internal/database/client.go @@ -6,7 +6,7 @@ import ( "slices" "time" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "gorm.io/gorm" "trpc.group/trpc-go/trpc-a2a-go/protocol" ) diff --git a/go/internal/database/fake/client.go b/go/internal/database/fake/client.go index de9701008..e9829bbec 100644 --- a/go/internal/database/fake/client.go +++ b/go/internal/database/fake/client.go @@ -6,7 +6,7 @@ import ( "sort" "sync" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/database" "gorm.io/gorm" "trpc.group/trpc-go/trpc-a2a-go/protocol" @@ -42,17 +42,6 @@ func NewClient() database.Client { nextFeedbackID: 1, } } -func (c *InMemoryFakeClient) messageKey(message *protocol.Message) string { - taskId := "none" - if message.TaskID != nil { - taskId = *message.TaskID - } - contextId := "none" - if message.ContextID != nil { - contextId = *message.ContextID - } - return fmt.Sprintf("%s_%s", taskId, contextId) -} func (c *InMemoryFakeClient) sessionKey(sessionID, userID string) string { return fmt.Sprintf("%s_%s", sessionID, userID) diff --git a/go/internal/httpserver/auth/authn.go b/go/internal/httpserver/auth/authn.go index ddaf49ad2..1068755ac 100644 --- a/go/internal/httpserver/auth/authn.go +++ b/go/internal/httpserver/auth/authn.go @@ -123,7 +123,7 @@ func A2ARequestHandler(authProvider AuthProvider) handler { var resp *http.Response defer func() { if err != nil && resp != nil { - resp.Body.Close() + resp.Body.Close() //nolint:errcheck } }() diff --git a/go/internal/httpserver/handlers/agents.go b/go/internal/httpserver/handlers/agents.go index 829c23890..2a92098d4 100644 --- a/go/internal/httpserver/handlers/agents.go +++ b/go/internal/httpserver/handlers/agents.go @@ -5,12 +5,11 @@ import ( "net/http" "github.com/go-logr/logr" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" - "github.com/kagent-dev/kagent/go/controller/translator" + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/internal/controller/translator" "github.com/kagent-dev/kagent/go/internal/httpserver/auth" "github.com/kagent-dev/kagent/go/internal/httpserver/errors" "github.com/kagent-dev/kagent/go/internal/utils" - common "github.com/kagent-dev/kagent/go/internal/utils" "github.com/kagent-dev/kagent/go/pkg/client/api" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" @@ -45,7 +44,7 @@ func (h *AgentsHandler) HandleListAgents(w ErrorResponseWriter, r *http.Request) agentsWithID := make([]api.AgentResponse, 0) for _, agent := range agentList.Items { - agentRef := common.GetObjectRef(&agent) + agentRef := utils.GetObjectRef(&agent) log.V(1).Info("Processing Agent", "agentRef", agentRef) agentResponse, err := h.getAgentResponse(r.Context(), log, &agent) @@ -64,7 +63,7 @@ func (h *AgentsHandler) HandleListAgents(w ErrorResponseWriter, r *http.Request) func (h *AgentsHandler) getAgentResponse(ctx context.Context, log logr.Logger, agent *v1alpha2.Agent) (api.AgentResponse, error) { - agentRef := common.GetObjectRef(agent) + agentRef := utils.GetObjectRef(agent) log.V(1).Info("Processing Agent", "agentRef", agentRef) deploymentReady := false @@ -76,7 +75,7 @@ func (h *AgentsHandler) getAgentResponse(ctx context.Context, log logr.Logger, a } response := api.AgentResponse{ - ID: common.ConvertToPythonIdentifier(agentRef), + ID: utils.ConvertToPythonIdentifier(agentRef), Agent: agent, DeploymentReady: deploymentReady, } @@ -102,7 +101,7 @@ func (h *AgentsHandler) getAgentResponse(ctx context.Context, log logr.Logger, a } response.ModelProvider = modelConfig.Spec.Provider response.Model = modelConfig.Spec.Model - response.ModelConfigRef = common.GetObjectRef(modelConfig) + response.ModelConfigRef = utils.GetObjectRef(modelConfig) response.Tools = agent.Spec.Declarative.Tools } @@ -165,11 +164,11 @@ func (h *AgentsHandler) HandleCreateAgent(w ErrorResponseWriter, r *http.Request return } if agentReq.Namespace == "" { - agentReq.Namespace = common.GetResourceNamespace() + agentReq.Namespace = utils.GetResourceNamespace() log.V(4).Info("Namespace not provided in request. Creating in controller installation namespace", "namespace", agentReq.Namespace) } - agentRef, err := common.ParseRefString(agentReq.Name, agentReq.Namespace) + agentRef, err := utils.ParseRefString(agentReq.Name, agentReq.Namespace) if err != nil { w.RespondWithError(errors.NewBadRequestError("Invalid agent metadata", err)) } @@ -185,7 +184,10 @@ func (h *AgentsHandler) HandleCreateAgent(w ErrorResponseWriter, r *http.Request } kubeClientWrapper := utils.NewKubeClientWrapper(h.KubeClient) - kubeClientWrapper.AddInMemory(&agentReq) + if err := kubeClientWrapper.AddInMemory(&agentReq); err != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to add Agent to Kubernetes wrapper", err)) + return + } apiTranslator := translator.NewAdkApiTranslator( kubeClientWrapper, @@ -222,11 +224,11 @@ func (h *AgentsHandler) HandleUpdateAgent(w ErrorResponseWriter, r *http.Request } if agentReq.Namespace == "" { - agentReq.Namespace = common.GetResourceNamespace() + agentReq.Namespace = utils.GetResourceNamespace() log.V(4).Info("Namespace not provided in request. Creating in controller installation namespace", "namespace", agentReq.Namespace) } - agentRef, err := common.ParseRefString(agentReq.Name, agentReq.Namespace) + agentRef, err := utils.ParseRefString(agentReq.Name, agentReq.Namespace) if err != nil { w.RespondWithError(errors.NewBadRequestError("Invalid Agent metadata", err)) } @@ -245,10 +247,7 @@ func (h *AgentsHandler) HandleUpdateAgent(w ErrorResponseWriter, r *http.Request existingAgent := &v1alpha2.Agent{} err = h.KubeClient.Get( r.Context(), - client.ObjectKey{ - Namespace: agentRef.Namespace, - Name: agentRef.Name, - }, + agentRef, existingAgent, ) if err != nil { diff --git a/go/internal/httpserver/handlers/agents_test.go b/go/internal/httpserver/handlers/agents_test.go index fc92cda2a..1285518bd 100644 --- a/go/internal/httpserver/handlers/agents_test.go +++ b/go/internal/httpserver/handlers/agents_test.go @@ -14,7 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/adk" "github.com/kagent-dev/kagent/go/internal/database" database_fake "github.com/kagent-dev/kagent/go/internal/database/fake" @@ -88,7 +88,7 @@ func createAgent(client database.Client, agent *v1alpha2.Agent) { Config: &adk.AgentConfig{}, ID: common.GetObjectRef(agent), } - client.StoreAgent(dbAgent) + client.StoreAgent(dbAgent) //nolint:errcheck } func TestHandleGetAgent(t *testing.T) { @@ -97,7 +97,7 @@ func TestHandleGetAgent(t *testing.T) { team := createTestAgent("test-team", modelConfig) handler, _ := setupTestHandler(team, modelConfig) - createAgent(handler.Base.DatabaseService, team) + createAgent(handler.DatabaseService, team) req := httptest.NewRequest("GET", "/api/agents/default/test-team", nil) req = mux.SetURLVars(req, map[string]string{"namespace": "default", "name": "test-team"}) @@ -135,7 +135,7 @@ func TestHandleGetAgent(t *testing.T) { agent := createTestAgentWithStatus("test-agent-ready", modelConfig, conditions) handler, _ := setupTestHandler(agent, modelConfig) - createAgent(handler.Base.DatabaseService, agent) + createAgent(handler.DatabaseService, agent) req := httptest.NewRequest("GET", "/api/agents/default/test-agent-ready", nil) req = mux.SetURLVars(req, map[string]string{"namespace": "default", "name": "test-agent-ready"}) @@ -164,7 +164,7 @@ func TestHandleGetAgent(t *testing.T) { agent := createTestAgentWithStatus("test-agent-not-ready", modelConfig, conditions) handler, _ := setupTestHandler(agent, modelConfig) - createAgent(handler.Base.DatabaseService, agent) + createAgent(handler.DatabaseService, agent) req := httptest.NewRequest("GET", "/api/agents/default/test-agent-not-ready", nil) req = mux.SetURLVars(req, map[string]string{"namespace": "default", "name": "test-agent-not-ready"}) @@ -193,7 +193,7 @@ func TestHandleGetAgent(t *testing.T) { agent := createTestAgentWithStatus("test-agent-different-reason", modelConfig, conditions) handler, _ := setupTestHandler(agent, modelConfig) - createAgent(handler.Base.DatabaseService, agent) + createAgent(handler.DatabaseService, agent) req := httptest.NewRequest("GET", "/api/agents/default/test-agent-different-reason", nil) req = mux.SetURLVars(req, map[string]string{"namespace": "default", "name": "test-agent-different-reason"}) @@ -242,8 +242,8 @@ func TestHandleListAgents(t *testing.T) { notReadyAgent := createTestAgent("not-ready-agent", modelConfig) handler, _ := setupTestHandler(readyAgent, notReadyAgent, modelConfig) - createAgent(handler.Base.DatabaseService, readyAgent) - createAgent(handler.Base.DatabaseService, notReadyAgent) + createAgent(handler.DatabaseService, readyAgent) + createAgent(handler.DatabaseService, notReadyAgent) req := httptest.NewRequest("GET", "/api/agents", nil) req = setUser(req, "test-user") @@ -384,7 +384,7 @@ func TestHandleDeleteTeam(t *testing.T) { } handler, _ := setupTestHandler(team) - createAgent(handler.Base.DatabaseService, team) + createAgent(handler.DatabaseService, team) req := httptest.NewRequest("DELETE", "/api/agents/default/test-team", nil) req = mux.SetURLVars(req, map[string]string{"namespace": "default", "name": "test-team"}) diff --git a/go/internal/httpserver/handlers/helpers.go b/go/internal/httpserver/handlers/helpers.go index 309b54b10..14d53acfe 100644 --- a/go/internal/httpserver/handlers/helpers.go +++ b/go/internal/httpserver/handlers/helpers.go @@ -36,7 +36,7 @@ func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) - w.Write(response) + w.Write(response) //nolint:errcheck log.V(2).Info("Sent JSON response", "statusCode", code, "responseSize", len(response)) } @@ -133,7 +133,7 @@ func DecodeJSONBody(r *http.Request, target interface{}) error { log.Info("Failed to decode JSON request body", "error", err.Error()) return err } - defer r.Body.Close() + defer r.Body.Close() //nolint:errcheck log.V(2).Info("Successfully decoded JSON request body") return nil diff --git a/go/internal/httpserver/handlers/memory.go b/go/internal/httpserver/handlers/memory.go index 4f3b8b053..a8c13735c 100644 --- a/go/internal/httpserver/handlers/memory.go +++ b/go/internal/httpserver/handlers/memory.go @@ -12,7 +12,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha1" "github.com/kagent-dev/kagent/go/internal/httpserver/auth" "github.com/kagent-dev/kagent/go/internal/httpserver/errors" common "github.com/kagent-dev/kagent/go/internal/utils" @@ -105,10 +105,7 @@ func (h *MemoryHandler) HandleCreateMemory(w ErrorResponseWriter, r *http.Reques existingMemory := &v1alpha1.Memory{} err = h.KubeClient.Get( r.Context(), - client.ObjectKey{ - Namespace: memoryRef.Namespace, - Name: memoryRef.Name, - }, + memoryRef, existingMemory, ) if err == nil { diff --git a/go/internal/httpserver/handlers/memory_test.go b/go/internal/httpserver/handlers/memory_test.go index 6832fc58c..c0b24993d 100644 --- a/go/internal/httpserver/handlers/memory_test.go +++ b/go/internal/httpserver/handlers/memory_test.go @@ -18,7 +18,7 @@ import ( ctrl_client "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha1" database_fake "github.com/kagent-dev/kagent/go/internal/database/fake" "github.com/kagent-dev/kagent/go/internal/httpserver/auth" "github.com/kagent-dev/kagent/go/internal/httpserver/handlers" diff --git a/go/internal/httpserver/handlers/modelconfig.go b/go/internal/httpserver/handlers/modelconfig.go index 355afb338..cd7234600 100644 --- a/go/internal/httpserver/handlers/modelconfig.go +++ b/go/internal/httpserver/handlers/modelconfig.go @@ -7,7 +7,7 @@ import ( "reflect" "strings" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/httpserver/auth" "github.com/kagent-dev/kagent/go/internal/httpserver/errors" common "github.com/kagent-dev/kagent/go/internal/utils" @@ -217,10 +217,7 @@ func (h *ModelConfigHandler) HandleCreateModelConfig(w ErrorResponseWriter, r *h existingConfig := &v1alpha2.ModelConfig{} err = h.KubeClient.Get( r.Context(), - client.ObjectKey{ - Namespace: modelConfigRef.Namespace, - Name: modelConfigRef.Name, - }, + modelConfigRef, existingConfig, ) if err == nil { diff --git a/go/internal/httpserver/handlers/modelconfig_test.go b/go/internal/httpserver/handlers/modelconfig_test.go index 27129b72e..daa80977d 100644 --- a/go/internal/httpserver/handlers/modelconfig_test.go +++ b/go/internal/httpserver/handlers/modelconfig_test.go @@ -18,7 +18,7 @@ import ( ctrl_client "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/httpserver/auth" "github.com/kagent-dev/kagent/go/internal/httpserver/handlers" "github.com/kagent-dev/kagent/go/pkg/client/api" diff --git a/go/internal/httpserver/handlers/models.go b/go/internal/httpserver/handlers/models.go index 4fb6c8043..b35119484 100644 --- a/go/internal/httpserver/handlers/models.go +++ b/go/internal/httpserver/handlers/models.go @@ -3,7 +3,7 @@ package handlers import ( "net/http" - v1alpha2 "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + v1alpha2 "github.com/kagent-dev/kagent/go/api/v1alpha2" kclient "github.com/kagent-dev/kagent/go/pkg/client" "github.com/kagent-dev/kagent/go/pkg/client/api" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" diff --git a/go/internal/httpserver/handlers/namespaces_test.go b/go/internal/httpserver/handlers/namespaces_test.go index a30ce982d..a89c0e8bd 100644 --- a/go/internal/httpserver/handlers/namespaces_test.go +++ b/go/internal/httpserver/handlers/namespaces_test.go @@ -16,7 +16,7 @@ import ( ctrl_client "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha1" "github.com/kagent-dev/kagent/go/internal/httpserver/handlers" "github.com/kagent-dev/kagent/go/pkg/client/api" ) diff --git a/go/internal/httpserver/handlers/providers.go b/go/internal/httpserver/handlers/providers.go index 8b99ff87d..482de2dc9 100644 --- a/go/internal/httpserver/handlers/providers.go +++ b/go/internal/httpserver/handlers/providers.go @@ -4,8 +4,8 @@ import ( "net/http" "reflect" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/pkg/client/api" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) diff --git a/go/internal/httpserver/handlers/sessions_test.go b/go/internal/httpserver/handlers/sessions_test.go index 49c65c391..1ff2c6714 100644 --- a/go/internal/httpserver/handlers/sessions_test.go +++ b/go/internal/httpserver/handlers/sessions_test.go @@ -15,7 +15,7 @@ import ( "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha1" "github.com/kagent-dev/kagent/go/internal/database" database_fake "github.com/kagent-dev/kagent/go/internal/database/fake" "github.com/kagent-dev/kagent/go/internal/httpserver/auth" @@ -56,7 +56,7 @@ func TestSessionsHandler(t *testing.T) { agent := &database.Agent{ ID: agentRef, } - dbClient.StoreAgent(agent) + dbClient.StoreAgent(agent) //nolint:errcheck // The fake client should assign an ID, but we'll use a default for testing agent.ID = "1" // Simulate the ID that would be assigned by GORM return agent @@ -69,7 +69,7 @@ func TestSessionsHandler(t *testing.T) { UserID: userID, AgentID: &agentID, } - dbClient.StoreSession(session) + dbClient.StoreSession(session) //nolint:errcheck return session } diff --git a/go/internal/httpserver/handlers/test_helpers_test.go b/go/internal/httpserver/handlers/test_helpers_test.go index 1eae09289..7e9afebf2 100644 --- a/go/internal/httpserver/handlers/test_helpers_test.go +++ b/go/internal/httpserver/handlers/test_helpers_test.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes/scheme" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/httpserver/errors" ) diff --git a/go/internal/httpserver/handlers/toolservers.go b/go/internal/httpserver/handlers/toolservers.go index df743896a..3b84ddf80 100644 --- a/go/internal/httpserver/handlers/toolservers.go +++ b/go/internal/httpserver/handlers/toolservers.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/go-logr/logr" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/httpserver/auth" "github.com/kagent-dev/kagent/go/internal/httpserver/errors" common "github.com/kagent-dev/kagent/go/internal/utils" diff --git a/go/internal/httpserver/handlers/toolservers_test.go b/go/internal/httpserver/handlers/toolservers_test.go index 06d3654ee..eb3fde475 100644 --- a/go/internal/httpserver/handlers/toolservers_test.go +++ b/go/internal/httpserver/handlers/toolservers_test.go @@ -19,7 +19,7 @@ import ( ctrl_client "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/database" database_fake "github.com/kagent-dev/kagent/go/internal/database/fake" "github.com/kagent-dev/kagent/go/internal/httpserver/auth" diff --git a/go/internal/httpserver/handlers/utils.go b/go/internal/httpserver/handlers/utils.go index a0c5de76c..8e20158a6 100644 --- a/go/internal/httpserver/handlers/utils.go +++ b/go/internal/httpserver/handlers/utils.go @@ -10,25 +10,10 @@ import ( "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" - common "github.com/kagent-dev/kagent/go/internal/utils" + "github.com/kagent-dev/kagent/go/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha2" ) -// Helper function to update a reference string -func updateRef(refPtr *string, namespace string) error { - if refPtr == nil { - return fmt.Errorf("reference pointer cannot be nil") - } - - ref, err := common.ParseRefString(*refPtr, namespace) - if err != nil { - return err - } - *refPtr = ref.String() - return nil -} - // createSecretWithOwnerReference creates a Kubernetes secret with owner reference. // Secret will have the same name and namespace as the owner object. func createSecretWithOwnerReference( diff --git a/go/internal/httpserver/middleware_error.go b/go/internal/httpserver/middleware_error.go index 4c2958935..f6593916b 100644 --- a/go/internal/httpserver/middleware_error.go +++ b/go/internal/httpserver/middleware_error.go @@ -63,5 +63,5 @@ func (w *errorResponseWriter) RespondWithError(err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) - json.NewEncoder(w).Encode(map[string]string{"error": responseMessage}) + json.NewEncoder(w).Encode(map[string]string{"error": responseMessage}) //nolint:errcheck } diff --git a/go/internal/httpserver/server.go b/go/internal/httpserver/server.go index 84df1fc5a..5c3b1d5e2 100644 --- a/go/internal/httpserver/server.go +++ b/go/internal/httpserver/server.go @@ -60,7 +60,6 @@ type HTTPServer struct { router *mux.Router handlers *handlers.Handlers dbManager *database.Manager - dbClient database.Client authenticator auth.AuthProvider } diff --git a/go/internal/utils/client_wrapper_test.go b/go/internal/utils/client_wrapper_test.go index 5bc5a5717..ff04f3a24 100644 --- a/go/internal/utils/client_wrapper_test.go +++ b/go/internal/utils/client_wrapper_test.go @@ -372,7 +372,7 @@ type mockObject struct { func (m *mockObject) DeepCopyObject() runtime.Object { return &mockObject{ TypeMeta: m.TypeMeta, - ObjectMeta: *m.ObjectMeta.DeepCopy(), + ObjectMeta: *m.DeepCopy(), Data: m.Data, } } diff --git a/go/controller/cmd/main.go b/go/pkg/app/app.go similarity index 80% rename from go/controller/cmd/main.go rename to go/pkg/app/app.go index 3e6b3b601..840cfc436 100644 --- a/go/controller/cmd/main.go +++ b/go/pkg/app/app.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package app import ( "context" @@ -25,19 +25,20 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/kagent-dev/kagent/go/internal/version" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/types" - "github.com/kagent-dev/kagent/go/controller/translator" "github.com/kagent-dev/kagent/go/internal/a2a" + "github.com/kagent-dev/kagent/go/internal/controller/translator" "github.com/kagent-dev/kagent/go/internal/database" versionmetrics "github.com/kagent-dev/kagent/go/internal/metrics" - a2a_reconciler "github.com/kagent-dev/kagent/go/controller/internal/a2a" - "github.com/kagent-dev/kagent/go/controller/internal/reconciler" + a2a_reconciler "github.com/kagent-dev/kagent/go/internal/controller/a2a" + "github.com/kagent-dev/kagent/go/internal/controller/reconciler" "github.com/kagent-dev/kagent/go/internal/httpserver" common "github.com/kagent-dev/kagent/go/internal/utils" @@ -59,11 +60,10 @@ import ( ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - "sigs.k8s.io/controller-runtime/pkg/webhook" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" - "github.com/kagent-dev/kagent/go/controller/internal/controller" + "github.com/kagent-dev/kagent/go/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/internal/controller" "github.com/kagent-dev/kagent/go/internal/goruntime" kmcpv1alpha1 "github.com/kagent-dev/kmcp/api/v1alpha1" kmcpcontroller "github.com/kagent-dev/kmcp/pkg/controller" @@ -99,14 +99,10 @@ type Config struct { CertName string CertKey string } - Webhook struct { - CertPath string - CertName string - CertKey string - } Streaming struct { MaxBufSize resource.QuantityValue `default:"1Mi"` InitialBufSize resource.QuantityValue `default:"4Ki"` + Timeout time.Duration `default:"60s"` } LeaderElection bool ProbeAddr string @@ -123,41 +119,42 @@ type Config struct { } } -// nolint:gocyclo -func main() { - cfg := Config{} - var tlsOpts []func(*tls.Config) - flag.StringVar(&cfg.Metrics.Addr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ +func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { + commandLine.StringVar(&cfg.Metrics.Addr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") - flag.StringVar(&cfg.ProbeAddr, "health-probe-bind-address", ":8082", "The address the probe endpoint binds to.") - flag.BoolVar(&cfg.LeaderElection, "leader-elect", false, + commandLine.StringVar(&cfg.ProbeAddr, "health-probe-bind-address", ":8082", "The address the probe endpoint binds to.") + commandLine.BoolVar(&cfg.LeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - flag.BoolVar(&cfg.SecureMetrics, "metrics-secure", true, + commandLine.BoolVar(&cfg.SecureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") - flag.StringVar(&cfg.Webhook.CertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") - flag.StringVar(&cfg.Webhook.CertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") - flag.StringVar(&cfg.Webhook.CertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") - flag.StringVar(&cfg.Metrics.CertPath, "metrics-cert-path", "", + commandLine.StringVar(&cfg.Metrics.CertPath, "metrics-cert-path", "", "The directory that contains the metrics server certificate.") - flag.StringVar(&cfg.Metrics.CertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") - flag.StringVar(&cfg.Metrics.CertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") - flag.BoolVar(&cfg.EnableHTTP2, "enable-http2", false, + commandLine.StringVar(&cfg.Metrics.CertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") + commandLine.StringVar(&cfg.Metrics.CertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") + commandLine.BoolVar(&cfg.EnableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") - flag.StringVar(&cfg.DefaultModelConfig.Name, "default-model-config-name", "default-model-config", "The name of the default model config.") - flag.StringVar(&cfg.DefaultModelConfig.Namespace, "default-model-config-namespace", kagentNamespace, "The namespace of the default model config.") - flag.StringVar(&cfg.HttpServerAddr, "http-server-address", ":8083", "The address the HTTP server binds to.") - flag.StringVar(&cfg.A2ABaseUrl, "a2a-base-url", "http://127.0.0.1:8083", "The base URL of the A2A Server endpoint, as advertised to clients.") - flag.StringVar(&cfg.Database.Type, "database-type", "sqlite", "The type of the database to use. Supported values: sqlite, postgres.") - flag.StringVar(&cfg.Database.Path, "sqlite-database-path", "./kagent.db", "The path to the SQLite database file.") - flag.StringVar(&cfg.Database.Url, "postgres-database-url", "postgres://postgres:kagent@db.kagent.svc.cluster.local:5432/crud", "The URL of the PostgreSQL database.") + commandLine.StringVar(&cfg.DefaultModelConfig.Name, "default-model-config-name", "default-model-config", "The name of the default model config.") + commandLine.StringVar(&cfg.DefaultModelConfig.Namespace, "default-model-config-namespace", kagentNamespace, "The namespace of the default model config.") + commandLine.StringVar(&cfg.HttpServerAddr, "http-server-address", ":8083", "The address the HTTP server binds to.") + commandLine.StringVar(&cfg.A2ABaseUrl, "a2a-base-url", "http://127.0.0.1:8083", "The base URL of the A2A Server endpoint, as advertised to clients.") + commandLine.StringVar(&cfg.Database.Type, "database-type", "sqlite", "The type of the database to use. Supported values: sqlite, postgres.") + commandLine.StringVar(&cfg.Database.Path, "sqlite-database-path", "./kagent.db", "The path to the SQLite database file.") + commandLine.StringVar(&cfg.Database.Url, "postgres-database-url", "postgres://postgres:kagent@db.kagent.svc.cluster.local:5432/crud", "The URL of the PostgreSQL database.") - flag.StringVar(&cfg.WatchNamespaces, "watch-namespaces", "", "The namespaces to watch for .") + commandLine.StringVar(&cfg.WatchNamespaces, "watch-namespaces", "", "The namespaces to watch for .") - flag.Var(&cfg.Streaming.MaxBufSize, "streaming-max-buf-size", "The maximum size of the streaming buffer.") - flag.Var(&cfg.Streaming.InitialBufSize, "streaming-initial-buf-size", "The initial size of the streaming buffer.") + commandLine.Var(&cfg.Streaming.MaxBufSize, "streaming-max-buf-size", "The maximum size of the streaming buffer.") + commandLine.Var(&cfg.Streaming.InitialBufSize, "streaming-initial-buf-size", "The initial size of the streaming buffer.") + commandLine.DurationVar(&cfg.Streaming.Timeout, "streaming-timeout", 60*time.Second, "The timeout for the streaming connection.") +} +func Start(authenticator auth.AuthProvider, authorizer auth.Authorizer) { + var tlsOpts []func(*tls.Config) + var cfg Config + + cfg.SetFlags(flag.CommandLine) flag.StringVar(&translator.DefaultImageConfig.Registry, "image-registry", translator.DefaultImageConfig.Registry, "The registry to use for the image.") flag.StringVar(&translator.DefaultImageConfig.Tag, "image-tag", translator.DefaultImageConfig.Tag, "The tag to use for the image.") flag.StringVar(&translator.DefaultImageConfig.PullPolicy, "image-pull-policy", translator.DefaultImageConfig.PullPolicy, "The pull policy to use for the image.") @@ -198,32 +195,6 @@ func main() { // Create watchers for metrics and webhooks certificates var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher - // Initial webhook TLS options - webhookTLSOpts := tlsOpts - - if len(cfg.Webhook.CertPath) > 0 { - setupLog.Info("Initializing webhook certificate watcher using provided certificates", - "webhook-cert-path", cfg.Webhook.CertPath, "webhook-cert-name", cfg.Webhook.CertName, "webhook-cert-key", cfg.Webhook.CertKey) - - var err error - webhookCertWatcher, err = certwatcher.New( - filepath.Join(cfg.Webhook.CertPath, cfg.Webhook.CertName), - filepath.Join(cfg.Webhook.CertPath, cfg.Webhook.CertKey), - ) - if err != nil { - setupLog.Error(err, "Failed to initialize webhook certificate watcher") - os.Exit(1) - } - - webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { - config.GetCertificate = webhookCertWatcher.GetCertificate - }) - } - - webhookServer := webhook.NewServer(webhook.Options{ - TLSOpts: webhookTLSOpts, - }) - ctrlmetrics.Registry.MustRegister(versionmetrics.NewBuildInfoCollector()) // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. @@ -277,7 +248,6 @@ func main() { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, - WebhookServer: webhookServer, HealthProbeBindAddress: cfg.ProbeAddr, LeaderElection: cfg.LeaderElection, LeaderElectionID: "0e9f6799.kagent.dev", @@ -329,16 +299,16 @@ func main() { cfg.DefaultModelConfig, ) - authorizer := &auth.NoopAuthorizer{} - authenticator := &auth.UnsecureAuthenticator{} - a2aHandler := a2a.NewA2AHttpMux(httpserver.APIPathA2A, authenticator) a2aReconciler := a2a_reconciler.NewReconciler( a2aHandler, cfg.A2ABaseUrl+httpserver.APIPathA2A, - int(cfg.Streaming.MaxBufSize.Value()), - int(cfg.Streaming.InitialBufSize.Value()), + a2a_reconciler.ClientOptions{ + StreamingMaxBufSize: int(cfg.Streaming.MaxBufSize.Value()), + StreamingInitialBufSize: int(cfg.Streaming.InitialBufSize.Value()), + Timeout: cfg.Streaming.Timeout, + }, authenticator, ) @@ -442,6 +412,10 @@ func main() { Authorizer: authorizer, Authenticator: authenticator, }) + if err != nil { + setupLog.Error(err, "unable to create HTTP server") + os.Exit(1) + } if err := mgr.Add(httpServer); err != nil { setupLog.Error(err, "unable to set up HTTP server") os.Exit(1) diff --git a/go/controller/cmd/main_test.go b/go/pkg/app/app_test.go similarity index 99% rename from go/controller/cmd/main_test.go rename to go/pkg/app/app_test.go index 3bc7bd653..d18991178 100644 --- a/go/controller/cmd/main_test.go +++ b/go/pkg/app/app_test.go @@ -1,4 +1,4 @@ -package main +package app import ( "strings" diff --git a/go/pkg/client/agent.go b/go/pkg/client/agent.go index 75341b2f7..4cc40e22b 100644 --- a/go/pkg/client/agent.go +++ b/go/pkg/client/agent.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/pkg/client/api" ) @@ -101,6 +101,6 @@ func (c *agentClient) DeleteAgent(ctx context.Context, agentRef string) error { if err != nil { return err } - resp.Body.Close() + resp.Body.Close() //nolint:errcheck return nil } diff --git a/go/pkg/client/api/types.go b/go/pkg/client/api/types.go index 03dd3d3d3..4ac440e74 100644 --- a/go/pkg/client/api/types.go +++ b/go/pkg/client/api/types.go @@ -1,8 +1,8 @@ package api import ( - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + "github.com/kagent-dev/kagent/go/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/internal/database" ) diff --git a/go/pkg/client/base.go b/go/pkg/client/base.go index dce95127f..f930496f7 100644 --- a/go/pkg/client/base.go +++ b/go/pkg/client/base.go @@ -113,7 +113,7 @@ func (c *BaseClient) doRequest(ctx context.Context, method, path string, body in if resp.StatusCode >= 400 { bodyBytes, _ := io.ReadAll(resp.Body) - resp.Body.Close() + resp.Body.Close() //nolint:errcheck var apiErr api.APIError if json.Unmarshal(bodyBytes, &apiErr) == nil && apiErr.Error != "" { @@ -151,7 +151,7 @@ func (c *BaseClient) Delete(ctx context.Context, path string, userID string) (*h } func DecodeResponse(resp *http.Response, target interface{}) error { - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck return json.NewDecoder(resp.Body).Decode(target) } diff --git a/go/pkg/client/feedback.go b/go/pkg/client/feedback.go index 8b3ffa68d..accb7a420 100644 --- a/go/pkg/client/feedback.go +++ b/go/pkg/client/feedback.go @@ -28,11 +28,10 @@ func (c *feedbackClient) CreateFeedback(ctx context.Context, feedback *api.Feedb userID = c.client.GetUserIDOrDefault(userID) feedback.UserID = userID - resp, err := c.client.Post(ctx, "/api/feedback", feedback, "") + _, err := c.client.Post(ctx, "/api/feedback", feedback, "") if err != nil { return err } - resp.Body.Close() return nil } diff --git a/go/pkg/client/health.go b/go/pkg/client/health.go index 513c1a4c0..5e27d54c1 100644 --- a/go/pkg/client/health.go +++ b/go/pkg/client/health.go @@ -21,10 +21,9 @@ func NewHealthClient(client *BaseClient) Health { // Health checks if the server is healthy func (c *healthClient) Get(ctx context.Context) error { - resp, err := c.client.Get(ctx, "/health", "") + _, err := c.client.Get(ctx, "/health", "") if err != nil { return err } - resp.Body.Close() return nil } diff --git a/go/pkg/client/memory.go b/go/pkg/client/memory.go index 26fe331a9..9cb425c7a 100644 --- a/go/pkg/client/memory.go +++ b/go/pkg/client/memory.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha1" "github.com/kagent-dev/kagent/go/pkg/client/api" ) @@ -92,10 +92,9 @@ func (c *memoryClient) UpdateMemory(ctx context.Context, namespace, memoryName s // DeleteMemory deletes a memory func (c *memoryClient) DeleteMemory(ctx context.Context, namespace, memoryName string) error { path := fmt.Sprintf("/api/memories/%s/%s", namespace, memoryName) - resp, err := c.client.Delete(ctx, path, "") + _, err := c.client.Delete(ctx, path, "") if err != nil { return err } - resp.Body.Close() return nil } diff --git a/go/pkg/client/model.go b/go/pkg/client/model.go index 9e9e7cb7c..2eabeb014 100644 --- a/go/pkg/client/model.go +++ b/go/pkg/client/model.go @@ -3,7 +3,7 @@ package client import ( "context" - v1alpha2 "github.com/kagent-dev/kagent/go/controller/api/v1alpha2" + v1alpha2 "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/pkg/client/api" ) diff --git a/go/pkg/client/modelconfig.go b/go/pkg/client/modelconfig.go index 19dfb01a0..b7324b2ab 100644 --- a/go/pkg/client/modelconfig.go +++ b/go/pkg/client/modelconfig.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha1" "github.com/kagent-dev/kagent/go/pkg/client/api" ) @@ -92,10 +92,9 @@ func (c *ModelConfigClient) UpdateModelConfig(ctx context.Context, namespace, co // DeleteModelConfig deletes a model configuration func (c *ModelConfigClient) DeleteModelConfig(ctx context.Context, namespace, configName string) error { path := fmt.Sprintf("/api/modelconfigs/%s/%s", namespace, configName) - resp, err := c.client.Delete(ctx, path, "") + _, err := c.client.Delete(ctx, path, "") if err != nil { return err } - resp.Body.Close() return nil } diff --git a/go/pkg/client/session.go b/go/pkg/client/session.go index 402dafac1..66440c08b 100644 --- a/go/pkg/client/session.go +++ b/go/pkg/client/session.go @@ -115,11 +115,10 @@ func (c *sessionClient) DeleteSession(ctx context.Context, sessionName string) e } path := fmt.Sprintf("/api/sessions/%s", sessionName) - resp, err := c.client.Delete(ctx, path, userID) + _, err := c.client.Delete(ctx, path, userID) if err != nil { return err } - resp.Body.Close() return nil } diff --git a/go/pkg/client/toolserver.go b/go/pkg/client/toolserver.go index 40bbb0b66..25c13b04b 100644 --- a/go/pkg/client/toolserver.go +++ b/go/pkg/client/toolserver.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + "github.com/kagent-dev/kagent/go/api/v1alpha1" "github.com/kagent-dev/kagent/go/pkg/client/api" ) @@ -58,10 +58,9 @@ func (c *ToolServerClient) CreateToolServer(ctx context.Context, toolServer *v1a // DeleteToolServer deletes a tool server func (c *ToolServerClient) DeleteToolServer(ctx context.Context, namespace, toolServerName string) error { path := fmt.Sprintf("/api/toolservers/%s/%s", namespace, toolServerName) - resp, err := c.client.Delete(ctx, path, "") + _, err := c.client.Delete(ctx, path, "") if err != nil { return err } - resp.Body.Close() return nil } diff --git a/go/pkg/sse/sse.go b/go/pkg/sse/sse.go index 9831e0f07..17eabfbd8 100644 --- a/go/pkg/sse/sse.go +++ b/go/pkg/sse/sse.go @@ -16,7 +16,7 @@ func StreamSseResponse(r io.ReadCloser) <-chan *Event { ch := make(chan *Event, 10) go func() { defer close(ch) - defer r.Close() + defer r.Close() //nolint:errcheck currentEvent := &Event{} for scanner.Scan() { line := scanner.Bytes() diff --git a/go/test/e2e/agents/kebab/README.md b/go/test/e2e/agents/kebab/README.md index 504d9178d..a601f392b 100644 --- a/go/test/e2e/agents/kebab/README.md +++ b/go/test/e2e/agents/kebab/README.md @@ -12,4 +12,18 @@ docker build . --push -t localhost:5001/kebab:latest ```bash kubectl apply -f agent.yaml +``` + +# Run manually + +You can run the agent manually for testing purposes. Make sure you have Python 3.11+ installed. + +```bash +cd go/test/e2e/agents/kebab +docker run --rm \ + -e KAGENT_URL=http://localhost:8083 \ + -e KAGENT_NAME=kebab-agent \ + -e KAGENT_NAMESPACE=kagent \ + --net=host \ + localhost:5001/kebab:latest ``` \ No newline at end of file diff --git a/go/test/e2e/agents/kebab/kebab/agent.py b/go/test/e2e/agents/kebab/kebab/agent.py index c24b150af..560e8148d 100644 --- a/go/test/e2e/agents/kebab/kebab/agent.py +++ b/go/test/e2e/agents/kebab/kebab/agent.py @@ -1,4 +1,5 @@ +import logging import random from google.adk.agents.invocation_context import InvocationContext @@ -7,6 +8,8 @@ from typing import AsyncGenerator, override from google.genai import types +logger = logging.getLogger(__name__) + class KebabAgent(BaseAgent): def __init__(self): @@ -27,8 +30,11 @@ async def _run_async_impl( Yields: Event: the events generated by the agent. """ - - text = "kebab" + session = ctx.session + text = f"kebab for {session.user_id} in session {session.id} " + + logger.info(f"Generating response: {text}") + model_response_event = Event( id=Event.new_id(), invocation_id=ctx.invocation_id, diff --git a/go/test/e2e/invoke_api_test.go b/go/test/e2e/invoke_api_test.go index 6607da18d..83d02e9bc 100644 --- a/go/test/e2e/invoke_api_test.go +++ b/go/test/e2e/invoke_api_test.go @@ -164,6 +164,6 @@ func TestInvokeExternalAgent(t *testing.T) { jsn, err := json.Marshal(taskResult) require.NoError(t, err) // Prime numbers - require.Contains(t, text, "kebab", string(jsn)) + require.Contains(t, text, "kebab for user@example.com", string(jsn)) }) } diff --git a/helm/README.md b/helm/README.md index c7816b400..4f967a50e 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 @@ -66,7 +64,6 @@ make kagent-cli-install export KAGENT_DEFAULT_MODEL_PROVIDER=ollama export KAGENT_HELM_REPO=./helm/ make kagent-cli-install - ``` ## Upgrading diff --git a/helm/kagent/templates/controller-deployment.yaml b/helm/kagent/templates/controller-deployment.yaml index 2ff7a0a63..9a1def876 100644 --- a/helm/kagent/templates/controller-deployment.yaml +++ b/helm/kagent/templates/controller-deployment.yaml @@ -54,6 +54,8 @@ spec: - {{ .Values.controller.streaming.maxBufSize | quote }} - -streaming-initial-buf-size - {{ .Values.controller.streaming.initialBufSize | quote }} + - -streaming-timeout + - {{ .Values.controller.streaming.timeout | quote }} - -database-type - {{ .Values.database.type }} {{- if eq .Values.database.type "sqlite" }} diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index 0399bda49..85dea2f3d 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -67,7 +67,7 @@ controller: streaming: # Streaming buffer size for A2A communication maxBufSize: 1Mi # 1024 * 1024 initialBufSize: 4Ki # 4 * 1024 - + timeout: 60s # 60 seconds # -- Namespaces the controller should watch. # If empty, the controller will watch ALL available namespaces. # @default -- [] (watches all available namespaces) diff --git a/python/packages/kagent-adk/src/kagent_adk/a2a.py b/python/packages/kagent-adk/src/kagent_adk/a2a.py index 52ef410ef..3f5388a1b 100644 --- a/python/packages/kagent-adk/src/kagent_adk/a2a.py +++ b/python/packages/kagent-adk/src/kagent_adk/a2a.py @@ -27,9 +27,6 @@ from ._task_store import KAgentTaskStore from ._token import KAgentTokenService -# --- Constants --- -USER_ID = "admin@kagent.dev" - # --- Configure Logging --- logger = logging.getLogger(__name__) @@ -40,7 +37,7 @@ def __init__(self, user_id: str): @property def is_authenticated(self) -> bool: - return False + return True @property def user_name(self) -> str: @@ -52,9 +49,8 @@ class KAgentRequestContextBuilder(SimpleRequestContextBuilder): A request context builder that will be used to hack in the user_id for now. """ - def __init__(self, user_id: str, task_store: TaskStore): + def __init__(self, task_store: TaskStore): super().__init__(task_store=task_store) - self.user_id = user_id async def build( self, @@ -64,10 +60,12 @@ async def build( task: Task | None = None, context: ServerCallContext | None = None, ) -> RequestContext: - if not context: - context = ServerCallContext(user=KAgentUser(user_id=self.user_id)) - else: - context.user = KAgentUser(user_id=self.user_id) + if context: + # grab the user id from the header + headers = context.state.get("headers", {}) + user_id = headers.get("x-user-id", None) + if user_id: + context.user = KAgentUser(user_id=user_id) request_context = await super().build(params, task_id, context_id, task, context) return request_context @@ -103,7 +101,7 @@ def __init__( def build(self) -> FastAPI: token_service = KAgentTokenService(self.app_name) - http_client = httpx.AsyncClient( + http_client = httpx.AsyncClient( # TODO: add user and agent headers base_url=kagent_url_override or self.kagent_url, event_hooks=token_service.event_hooks() ) session_service = KAgentSessionService(http_client) @@ -121,7 +119,7 @@ def create_runner() -> Runner: kagent_task_store = KAgentTaskStore(http_client) - request_context_builder = KAgentRequestContextBuilder(user_id=USER_ID, task_store=kagent_task_store) + request_context_builder = KAgentRequestContextBuilder(task_store=kagent_task_store) request_handler = DefaultRequestHandler( agent_executor=agent_executor, task_store=kagent_task_store, diff --git a/ui/package-lock.json b/ui/package-lock.json index 007edc535..5d0bd8cd3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,8 +8,8 @@ "name": "kagents-ui", "version": "0.1.0", "dependencies": { - "@a2a-js/sdk": "^0.2.4", - "@hookform/resolvers": "^5.2.0", + "@a2a-js/sdk": "^0.2.5", + "@hookform/resolvers": "^5.2.1", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-checkbox": "^1.3.2", @@ -32,7 +32,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", - "lucide-react": "^0.525.0", + "lucide-react": "^0.534.0", "next": "^15.4.5", "next-themes": "^0.4.6", "react": "^18.3.1", @@ -46,7 +46,7 @@ "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", "zod": "^3.25.76", - "zustand": "^5.0.6" + "zustand": "^5.0.7" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", @@ -74,9 +74,9 @@ } }, "node_modules/@a2a-js/sdk": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.2.4.tgz", - "integrity": "sha512-s9wEF5SUswhaAeAERA3tIBcrYEqWfkf+B3yiofxFX8+wnJMQL2l6bT6e7LZqjFf8sup0IRqFtGbckBPDLQymjw==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.2.5.tgz", + "integrity": "sha512-VTDuRS5V0ATbJ/LkaQlisMnTAeYKXAK6scMguVBstf+KIBQ7HIuKhiXLv+G/hvejkV+THoXzoNifInAkU81P1g==", "dependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.23", @@ -952,9 +952,9 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" }, "node_modules/@hookform/resolvers": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.0.tgz", - "integrity": "sha512-3YI+VqxJQH6ryRWG+j3k+M19Wf37LeSKJDg6Vdjq6makLOqZGYn77iTaYLMLpVi/uHc1N6OTCmcxJwhOQV979g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.1.tgz", + "integrity": "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==", "license": "MIT", "dependencies": { "@standard-schema/utils": "^0.3.0" @@ -10905,9 +10905,9 @@ "license": "ISC" }, "node_modules/lucide-react": { - "version": "0.525.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", - "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", + "version": "0.534.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.534.0.tgz", + "integrity": "sha512-4Bz7rujQ/mXHqCwjx09ih/Q9SCizz9CjBV5repw9YSHZZZaop9/Oj0RgCDt6WdEaeAPfbcZ8l2b4jzApStqgNw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -15939,9 +15939,10 @@ } }, "node_modules/zustand": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.6.tgz", - "integrity": "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", "engines": { "node": ">=12.20.0" }, diff --git a/ui/package.json b/ui/package.json index 30f315bfc..82ea66e20 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,8 +11,8 @@ "test:watch": "jest --watch" }, "dependencies": { - "@a2a-js/sdk": "^0.2.4", - "@hookform/resolvers": "^5.2.0", + "@a2a-js/sdk": "^0.2.5", + "@hookform/resolvers": "^5.2.1", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-checkbox": "^1.3.2", @@ -35,7 +35,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", - "lucide-react": "^0.525.0", + "lucide-react": "^0.534.0", "next": "^15.4.5", "next-themes": "^0.4.6", "react": "^18.3.1", @@ -49,7 +49,7 @@ "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", "zod": "^3.25.76", - "zustand": "^5.0.6" + "zustand": "^5.0.7" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1",