From 12042cae4c3a1de100bf586979eebe47601a1adc Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 17:34:41 +0200 Subject: [PATCH 01/41] fix all linter errors Signed-off-by: Dmytro Rashko --- Makefile | 56 +++++++++++++++-- internal/version/version.go | 1 - pkg/argo/argo.go | 9 +-- pkg/cilium/cilium.go | 25 -------- pkg/k8s/k8s.go | 99 ++----------------------------- pkg/prometheus/prometheus_test.go | 26 -------- pkg/utils/datetime_test.go | 2 +- 7 files changed, 59 insertions(+), 159 deletions(-) diff --git a/Makefile b/Makefile index 86f1282b..a4087f46 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,10 @@ GIT_COMMIT := $(shell git rev-parse --short HEAD || echo "unknown") VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null | sed 's/-dirty//' | grep v || echo "v0.0.0-$(GIT_COMMIT)") # Version information for the build -LDFLAGS := "-X github.com/kagent-dev/tools/internal/version.Version=$(VERSION) \ - -X github.com/kagent-dev/tools/internal/version.GitCommit=$(GIT_COMMIT) \ - -X github.com/kagent-dev/tools/internal/version.BuildDate=$(BUILD_DATE)" +LDFLAGS := -X github.com/kagent-dev/tools/internal/version.Version=$(VERSION) -X github.com/kagent-dev/tools/internal/version.GitCommit=$(GIT_COMMIT) -X github.com/kagent-dev/tools/internal/version.BuildDate=$(BUILD_DATE) + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin .PHONY: fmt fmt: ## Run go fmt against code. @@ -31,6 +32,18 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes lint-config: golangci-lint ## Verify golangci-lint linter configuration $(GOLANGCI_LINT) config verify +.PHONY: govulncheck +govulncheck: + $(call go-install-tool,bin/govulncheck,golang.org/x/vuln/cmd/govulncheck,latest) + ./bin/govulncheck-latest ./... + +.PHONY: tidy +tidy: ## Run go mod tidy to ensure dependencies are up to date. + go mod tidy + +.PHONY: test +test: + go test -v -cover ./... bin/kagent-tools-linux-amd64: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/kagent-tools-linux-amd64 ./cmd @@ -63,7 +76,7 @@ bin/kagent-tools-windows-amd64.exe.sha256: bin/kagent-tools-windows-amd64.exe sha256sum bin/kagent-tools-windows-amd64.exe > bin/kagent-tools-windows-amd64.exe.sha256 .PHONY: build -build: bin/kagent-tools-linux-amd64.sha256 bin/kagent-tools-linux-arm64.sha256 bin/kagent-tools-darwin-amd64.sha256 bin/kagent-tools-darwin-arm64.sha256 bin/kagent-tools-windows-amd64.exe.sha256 +build: $(LOCALBIN) tidy fmt lint bin/kagent-tools-linux-amd64.sha256 bin/kagent-tools-linux-arm64.sha256 bin/kagent-tools-darwin-amd64.sha256 bin/kagent-tools-darwin-arm64.sha256 bin/kagent-tools-windows-amd64.exe.sha256 TOOLS_IMAGE_NAME ?= tools TOOLS_IMAGE_TAG ?= $(VERSION) @@ -84,12 +97,43 @@ TOOLS_HELM_VERSION ?= 3.18.3 # build args TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION) -TOOLS_IMAGE_BUILD_ARGS += --build-arg LDFLAGS=$(LDFLAGS) +TOOLS_IMAGE_BUILD_ARGS += --build-arg LDFLAGS="$(LDFLAGS)" TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_ISTIO_VERSION=$(TOOLS_ISTIO_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_ARGO_ROLLOUTS_VERSION=$(TOOLS_ARGO_ROLLOUTS_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_KUBECTL_VERSION=$(TOOLS_KUBECTL_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_HELM_VERSION=$(TOOLS_HELM_VERSION) .PHONY: docker-build # build tools image -docker-build: +docker-build: fmt $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f Dockerfile ./ + +## Tool Binaries +## Location to install dependencies t + +.PHONY: $(LOCALBIN) +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint +GOLANGCI_LINT_VERSION ?= v1.63.4 + +.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)) + +# 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 +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f "$(1)-$(3)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f $(1) || true ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv $(1) $(1)-$(3) ;\ +} ;\ +ln -sf $(1)-$(3) $(1) +endef \ No newline at end of file diff --git a/internal/version/version.go b/internal/version/version.go index b43bc838..6b556e1b 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -6,7 +6,6 @@ import ( ) var ( - // These variables should be set during build time using -ldflags Version = "dev" GitCommit = "none" BuildDate = "unknown" diff --git a/pkg/argo/argo.go b/pkg/argo/argo.go index 71328c58..764e10b1 100644 --- a/pkg/argo/argo.go +++ b/pkg/argo/argo.go @@ -300,13 +300,8 @@ func handleVerifyGatewayPlugin(ctx context.Context, request mcp.CallToolRequest) func handleCheckPluginLogs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { namespace := mcp.ParseString(request, "namespace", "argo-rollouts") - timeoutStr := mcp.ParseString(request, "timeout", "60") - - // Parse timeout (for potential future use) - _, err := strconv.Atoi(timeoutStr) - if err != nil { - // Use default timeout of 60 if parsing fails - } + // timeout parameter is parsed but not used currently + _ = mcp.ParseString(request, "timeout", "60") cmd := []string{"logs", "-n", namespace, "-l", "app.kubernetes.io/name=argo-rollouts", "--tail", "100"} output, err := utils.RunCommandWithContext(ctx, "kubectl", cmd) diff --git a/pkg/cilium/cilium.go b/pkg/cilium/cilium.go index 9fdbfe50..de3cec99 100644 --- a/pkg/cilium/cilium.go +++ b/pkg/cilium/cilium.go @@ -333,10 +333,6 @@ func RegisterCiliumTools(s *server.MCPServer) { // -- Debug Tools -- -func getCiliumPodName(nodeName string) (string, error) { - return getCiliumPodNameWithContext(context.Background(), nodeName) -} - func getCiliumPodNameWithContext(ctx context.Context, nodeName string) (string, error) { args := []string{"get", "pod", "-l", "k8s-app=cilium", "-o", "name", "-n", "kube-system"} if nodeName != "" { @@ -1032,27 +1028,6 @@ func handleGetServiceInformation(ctx context.Context, request mcp.CallToolReques return mcp.NewToolResultText(output), nil } -func handleDeleteService(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - serviceID := mcp.ParseString(request, "service_id", "") - all := mcp.ParseString(request, "all", "") == "true" - nodeName := mcp.ParseString(request, "node_name", "") - - var cmd string - if all { - cmd = "service delete --all" - } else if serviceID != "" { - cmd = fmt.Sprintf("service delete %s", serviceID) - } else { - return mcp.NewToolResultError("either service_id or all=true must be provided"), nil - } - - output, err := runCiliumDbgCommand(cmd, nodeName) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete service: %v", err)), nil - } - return mcp.NewToolResultText(output), nil -} - func handleUpdateService(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { backendWeights := mcp.ParseString(request, "backend_weights", "") backends := mcp.ParseString(request, "backends", "") diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index ee80a32e..d9661028 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -19,7 +19,6 @@ import ( "github.com/mark3labs/mcp-go/server" "github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms/openai" - v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -69,97 +68,6 @@ func NewK8sTool(llmModel llms.Model) (*K8sTool, error) { return &K8sTool{client: client, llmModel: llmModel}, nil } -func (k *K8sTool) getPodsNative(ctx context.Context, name, namespace string, allNamespaces bool, output string) (*mcp.CallToolResult, error) { - var pods *corev1.PodList - var err error - - if name != "" { - pod, err := k.client.clientset.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get pod: %v", err)), nil - } - pods = &corev1.PodList{Items: []corev1.Pod{*pod}} - } else if allNamespaces { - pods, err = k.client.clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) - } else { - pods, err = k.client.clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) - } - - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list pods: %v", err)), nil - } - - return formatResourceOutput(pods, output) -} - -func (k *K8sTool) getServicesNative(ctx context.Context, name, namespace string, allNamespaces bool, output string) (*mcp.CallToolResult, error) { - var services *corev1.ServiceList - var err error - - if name != "" { - service, err := k.client.clientset.CoreV1().Services(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get service: %v", err)), nil - } - services = &corev1.ServiceList{Items: []corev1.Service{*service}} - } else if allNamespaces { - services, err = k.client.clientset.CoreV1().Services("").List(ctx, metav1.ListOptions{}) - } else { - services, err = k.client.clientset.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{}) - } - - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list services: %v", err)), nil - } - - return formatResourceOutput(services, output) -} - -func (k *K8sTool) getDeploymentsNative(ctx context.Context, name, namespace string, allNamespaces bool, output string) (*mcp.CallToolResult, error) { - var deployments *v1.DeploymentList - var err error - - if name != "" { - deployment, err := k.client.clientset.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get deployment: %v", err)), nil - } - deployments = &v1.DeploymentList{Items: []v1.Deployment{*deployment}} - } else if allNamespaces { - deployments, err = k.client.clientset.AppsV1().Deployments("").List(ctx, metav1.ListOptions{}) - } else { - deployments, err = k.client.clientset.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{}) - } - - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list deployments: %v", err)), nil - } - - return formatResourceOutput(deployments, output) -} - -func (k *K8sTool) getConfigMapsNative(ctx context.Context, name, namespace string, allNamespaces bool, output string) (*mcp.CallToolResult, error) { - var configMaps *corev1.ConfigMapList - var err error - - if name != "" { - configMap, err := k.client.clientset.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get configmap: %v", err)), nil - } - configMaps = &corev1.ConfigMapList{Items: []corev1.ConfigMap{*configMap}} - } else if allNamespaces { - configMaps, err = k.client.clientset.CoreV1().ConfigMaps("").List(ctx, metav1.ListOptions{}) - } else { - configMaps, err = k.client.clientset.CoreV1().ConfigMaps(namespace).List(ctx, metav1.ListOptions{}) - } - - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list configmaps: %v", err)), nil - } - - return formatResourceOutput(configMaps, output) -} func formatResourceOutput(data interface{}, output string) (*mcp.CallToolResult, error) { if output == "json" || output == "" { @@ -321,7 +229,12 @@ func (k *K8sTool) handleCheckServiceConnectivity(ctx context.Context, request mc // This is a complex operation to perform natively, involving creating a temporary pod. // We'll keep the kubectl approach for this tool for now. podName := fmt.Sprintf("curl-test-%d", rand.Intn(10000)) - defer k.runKubectlCommand(ctx, []string{"delete", "pod", podName, "-n", namespace, "--ignore-not-found"}) + defer func() { + if _, err := k.runKubectlCommand(ctx, []string{"delete", "pod", podName, "-n", namespace, "--ignore-not-found"}); err != nil { + // Log the error but don't fail the operation + fmt.Printf("Warning: Failed to cleanup pod %s: %v\n", podName, err) + } + }() _, err := k.runKubectlCommand(ctx, []string{"run", podName, "--image=curlimages/curl", "-n", namespace, "--restart=Never", "--", "sleep", "3600"}) if err != nil { diff --git a/pkg/prometheus/prometheus_test.go b/pkg/prometheus/prometheus_test.go index d0246ac3..7b1b4c03 100644 --- a/pkg/prometheus/prometheus_test.go +++ b/pkg/prometheus/prometheus_test.go @@ -1,27 +1 @@ package prometheus - -import ( - "net/http" -) - -// mockRoundTripper is used to mock HTTP responses for testing -type mockRoundTripper struct { - response *http.Response - err error -} - -func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - if m.err != nil { - return nil, m.err - } - return m.response, nil -} - -func newTestClient(response *http.Response, err error) *http.Client { - return &http.Client{ - Transport: &mockRoundTripper{ - response: response, - err: err, - }, - } -} diff --git a/pkg/utils/datetime_test.go b/pkg/utils/datetime_test.go index 6b542599..8f1cd641 100644 --- a/pkg/utils/datetime_test.go +++ b/pkg/utils/datetime_test.go @@ -24,7 +24,7 @@ func TestHandleGetCurrentDateTimeTool(t *testing.T) { t.Fatal("Expected non-nil result") } - if result.Content == nil || len(result.Content) == 0 { + if len(result.Content) == 0 { t.Fatal("Expected content in result") } From 40b8c8bd3530bc02911e11491cb2677a30714de3 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 17:39:38 +0200 Subject: [PATCH 02/41] fix all linter errors Signed-off-by: Dmytro Rashko --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 845a2630..5d511f8d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -50,6 +50,6 @@ jobs: cache: true - name: Run cmd/main.go tests - working-directory: go + working-directory: . run: | go test -v ./... From 6fdea512970526fb84e87716144592d3503ef168 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 18:02:55 +0200 Subject: [PATCH 03/41] add buildx Signed-off-by: Dmytro Rashko --- .github/workflows/ci.yaml | 1 + Dockerfile | 8 +++++--- Makefile | 26 ++++++++++++++++++++++---- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5d511f8d..a857e6e2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,6 +27,7 @@ jobs: - name: Run make build env: DOCKER_BUILDKIT: 1 + BUILDX_BUILDER_NAME: kagent-builder-v0.23.0 DOCKER_BUILD_ARGS: >- --cache-from=type=gha --cache-to=type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 0e361a12..e307408a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,10 +40,11 @@ RUN curl -Lo /downloads/kubectl-argo-rollouts https://github.com/argoproj/argo-r ### STAGE 2: build-tools MCP ARG BASE_IMAGE_REGISTRY=cgr.dev +ARG BUILDARCH=amd64 FROM --platform=linux/$BUILDARCH $BASE_IMAGE_REGISTRY/chainguard/go:latest AS builder - ARG TARGETPLATFORM ARG TARGETARCH +ARG BUILDARCH ARG LDFLAGS WORKDIR /workspace @@ -68,8 +69,9 @@ COPY pkg pkg # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN --mount=type=cache,target=/root/go/pkg/mod,rw \ - --mount=type=cache,target=/root/.cache/go-build,rw \ +RUN --mount=type=cache,target=/root/go/pkg/mod,rw \ + --mount=type=cache,target=/root/.cache/go-build,rw \ + echo "Building tool-server for $TARGETARCH on $BUILDARCH" && \ CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -ldflags "$LDFLAGS" -o tool-server cmd/main.go # Use distroless as minimal base image to package the manager binary diff --git a/Makefile b/Makefile index a4087f46..c75a1843 100644 --- a/Makefile +++ b/Makefile @@ -87,10 +87,16 @@ RETAGGED_TOOLS_IMG = $(RETAGGED_DOCKER_REGISTRY)/$(DOCKER_REPO)/$(TOOLS_IMAGE_NA LOCALARCH ?= $(shell uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/') -DOCKER_BUILDER ?= docker -DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) +#buildx settings +BUILDKIT_VERSION = v0.23.0 +BUILDX_NO_DEFAULT_ATTESTATIONS=1 +BUILDX_BUILDER_NAME ?= kagent-builder-$(BUILDKIT_VERSION) -TOOLS_ISTIO_VERSION ?= 1.26.1 +DOCKER_BUILDER ?= docker buildx +DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUILDX_BUILDER_NAME) + +# tools image build args +TOOLS_ISTIO_VERSION ?= 1.26.2 TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 TOOLS_KUBECTL_VERSION ?= 1.33.2 TOOLS_HELM_VERSION ?= 3.18.3 @@ -98,13 +104,25 @@ TOOLS_HELM_VERSION ?= 3.18.3 # build args TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg LDFLAGS="$(LDFLAGS)" +TOOLS_IMAGE_BUILD_ARGS += --build-arg LOCALARCH=$(LOCALARCH) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_ISTIO_VERSION=$(TOOLS_ISTIO_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_ARGO_ROLLOUTS_VERSION=$(TOOLS_ARGO_ROLLOUTS_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_KUBECTL_VERSION=$(TOOLS_KUBECTL_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_HELM_VERSION=$(TOOLS_HELM_VERSION) +.PHONY: buildx-create +buildx-create: + docker buildx inspect $(BUILDX_BUILDER_NAME) 2>&1 > /dev/null || \ + docker buildx create --name $(BUILDX_BUILDER_NAME) --platform linux/amd64,linux/arm64 --driver docker-container --use || true + .PHONY: docker-build # build tools image -docker-build: fmt +docker-build: fmt buildx-create + $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f Dockerfile ./ + +.PHONY: docker-build # build tools image for amd64 and arm64 +docker-build-all: fmt buildx-create +docker-build-all: DOCKER_BUILD_ARGS = --progress=plain --builder $(BUILDX_BUILDER_NAME) --platform linux/amd64,linux/arm64 --output type=tar,dest=/dev/null +docker-build-all: $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f Dockerfile ./ ## Tool Binaries From 1713ac677b7fe67dc0d0a8af99ceafeba4e2e9ee Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 18:24:36 +0200 Subject: [PATCH 04/41] fix all linter errors (#1) * fix all linter errors * add buildx --------- Signed-off-by: Dmytro Rashko --- .github/workflows/ci.yaml | 3 +- Dockerfile | 8 ++- Makefile | 80 ++++++++++++++++++++++--- internal/version/version.go | 1 - pkg/argo/argo.go | 9 +-- pkg/cilium/cilium.go | 25 -------- pkg/k8s/k8s.go | 99 ++----------------------------- pkg/prometheus/prometheus_test.go | 26 -------- pkg/utils/datetime_test.go | 2 +- 9 files changed, 87 insertions(+), 166 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 845a2630..a857e6e2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,6 +27,7 @@ jobs: - name: Run make build env: DOCKER_BUILDKIT: 1 + BUILDX_BUILDER_NAME: kagent-builder-v0.23.0 DOCKER_BUILD_ARGS: >- --cache-from=type=gha --cache-to=type=gha,mode=max @@ -50,6 +51,6 @@ jobs: cache: true - name: Run cmd/main.go tests - working-directory: go + working-directory: . run: | go test -v ./... diff --git a/Dockerfile b/Dockerfile index 0e361a12..e307408a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,10 +40,11 @@ RUN curl -Lo /downloads/kubectl-argo-rollouts https://github.com/argoproj/argo-r ### STAGE 2: build-tools MCP ARG BASE_IMAGE_REGISTRY=cgr.dev +ARG BUILDARCH=amd64 FROM --platform=linux/$BUILDARCH $BASE_IMAGE_REGISTRY/chainguard/go:latest AS builder - ARG TARGETPLATFORM ARG TARGETARCH +ARG BUILDARCH ARG LDFLAGS WORKDIR /workspace @@ -68,8 +69,9 @@ COPY pkg pkg # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN --mount=type=cache,target=/root/go/pkg/mod,rw \ - --mount=type=cache,target=/root/.cache/go-build,rw \ +RUN --mount=type=cache,target=/root/go/pkg/mod,rw \ + --mount=type=cache,target=/root/.cache/go-build,rw \ + echo "Building tool-server for $TARGETARCH on $BUILDARCH" && \ CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -ldflags "$LDFLAGS" -o tool-server cmd/main.go # Use distroless as minimal base image to package the manager binary diff --git a/Makefile b/Makefile index 86f1282b..c75a1843 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,10 @@ GIT_COMMIT := $(shell git rev-parse --short HEAD || echo "unknown") VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null | sed 's/-dirty//' | grep v || echo "v0.0.0-$(GIT_COMMIT)") # Version information for the build -LDFLAGS := "-X github.com/kagent-dev/tools/internal/version.Version=$(VERSION) \ - -X github.com/kagent-dev/tools/internal/version.GitCommit=$(GIT_COMMIT) \ - -X github.com/kagent-dev/tools/internal/version.BuildDate=$(BUILD_DATE)" +LDFLAGS := -X github.com/kagent-dev/tools/internal/version.Version=$(VERSION) -X github.com/kagent-dev/tools/internal/version.GitCommit=$(GIT_COMMIT) -X github.com/kagent-dev/tools/internal/version.BuildDate=$(BUILD_DATE) + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin .PHONY: fmt fmt: ## Run go fmt against code. @@ -31,6 +32,18 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes lint-config: golangci-lint ## Verify golangci-lint linter configuration $(GOLANGCI_LINT) config verify +.PHONY: govulncheck +govulncheck: + $(call go-install-tool,bin/govulncheck,golang.org/x/vuln/cmd/govulncheck,latest) + ./bin/govulncheck-latest ./... + +.PHONY: tidy +tidy: ## Run go mod tidy to ensure dependencies are up to date. + go mod tidy + +.PHONY: test +test: + go test -v -cover ./... bin/kagent-tools-linux-amd64: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/kagent-tools-linux-amd64 ./cmd @@ -63,7 +76,7 @@ bin/kagent-tools-windows-amd64.exe.sha256: bin/kagent-tools-windows-amd64.exe sha256sum bin/kagent-tools-windows-amd64.exe > bin/kagent-tools-windows-amd64.exe.sha256 .PHONY: build -build: bin/kagent-tools-linux-amd64.sha256 bin/kagent-tools-linux-arm64.sha256 bin/kagent-tools-darwin-amd64.sha256 bin/kagent-tools-darwin-arm64.sha256 bin/kagent-tools-windows-amd64.exe.sha256 +build: $(LOCALBIN) tidy fmt lint bin/kagent-tools-linux-amd64.sha256 bin/kagent-tools-linux-arm64.sha256 bin/kagent-tools-darwin-amd64.sha256 bin/kagent-tools-darwin-arm64.sha256 bin/kagent-tools-windows-amd64.exe.sha256 TOOLS_IMAGE_NAME ?= tools TOOLS_IMAGE_TAG ?= $(VERSION) @@ -74,22 +87,71 @@ RETAGGED_TOOLS_IMG = $(RETAGGED_DOCKER_REGISTRY)/$(DOCKER_REPO)/$(TOOLS_IMAGE_NA LOCALARCH ?= $(shell uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/') -DOCKER_BUILDER ?= docker -DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) +#buildx settings +BUILDKIT_VERSION = v0.23.0 +BUILDX_NO_DEFAULT_ATTESTATIONS=1 +BUILDX_BUILDER_NAME ?= kagent-builder-$(BUILDKIT_VERSION) + +DOCKER_BUILDER ?= docker buildx +DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUILDX_BUILDER_NAME) -TOOLS_ISTIO_VERSION ?= 1.26.1 +# tools image build args +TOOLS_ISTIO_VERSION ?= 1.26.2 TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 TOOLS_KUBECTL_VERSION ?= 1.33.2 TOOLS_HELM_VERSION ?= 3.18.3 # build args TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION) -TOOLS_IMAGE_BUILD_ARGS += --build-arg LDFLAGS=$(LDFLAGS) +TOOLS_IMAGE_BUILD_ARGS += --build-arg LDFLAGS="$(LDFLAGS)" +TOOLS_IMAGE_BUILD_ARGS += --build-arg LOCALARCH=$(LOCALARCH) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_ISTIO_VERSION=$(TOOLS_ISTIO_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_ARGO_ROLLOUTS_VERSION=$(TOOLS_ARGO_ROLLOUTS_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_KUBECTL_VERSION=$(TOOLS_KUBECTL_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_HELM_VERSION=$(TOOLS_HELM_VERSION) +.PHONY: buildx-create +buildx-create: + docker buildx inspect $(BUILDX_BUILDER_NAME) 2>&1 > /dev/null || \ + docker buildx create --name $(BUILDX_BUILDER_NAME) --platform linux/amd64,linux/arm64 --driver docker-container --use || true + .PHONY: docker-build # build tools image -docker-build: +docker-build: fmt buildx-create + $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f Dockerfile ./ + +.PHONY: docker-build # build tools image for amd64 and arm64 +docker-build-all: fmt buildx-create +docker-build-all: DOCKER_BUILD_ARGS = --progress=plain --builder $(BUILDX_BUILDER_NAME) --platform linux/amd64,linux/arm64 --output type=tar,dest=/dev/null +docker-build-all: $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f Dockerfile ./ + +## Tool Binaries +## Location to install dependencies t + +.PHONY: $(LOCALBIN) +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint +GOLANGCI_LINT_VERSION ?= v1.63.4 + +.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)) + +# 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 +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f "$(1)-$(3)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f $(1) || true ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv $(1) $(1)-$(3) ;\ +} ;\ +ln -sf $(1)-$(3) $(1) +endef \ No newline at end of file diff --git a/internal/version/version.go b/internal/version/version.go index b43bc838..6b556e1b 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -6,7 +6,6 @@ import ( ) var ( - // These variables should be set during build time using -ldflags Version = "dev" GitCommit = "none" BuildDate = "unknown" diff --git a/pkg/argo/argo.go b/pkg/argo/argo.go index 71328c58..764e10b1 100644 --- a/pkg/argo/argo.go +++ b/pkg/argo/argo.go @@ -300,13 +300,8 @@ func handleVerifyGatewayPlugin(ctx context.Context, request mcp.CallToolRequest) func handleCheckPluginLogs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { namespace := mcp.ParseString(request, "namespace", "argo-rollouts") - timeoutStr := mcp.ParseString(request, "timeout", "60") - - // Parse timeout (for potential future use) - _, err := strconv.Atoi(timeoutStr) - if err != nil { - // Use default timeout of 60 if parsing fails - } + // timeout parameter is parsed but not used currently + _ = mcp.ParseString(request, "timeout", "60") cmd := []string{"logs", "-n", namespace, "-l", "app.kubernetes.io/name=argo-rollouts", "--tail", "100"} output, err := utils.RunCommandWithContext(ctx, "kubectl", cmd) diff --git a/pkg/cilium/cilium.go b/pkg/cilium/cilium.go index 9fdbfe50..de3cec99 100644 --- a/pkg/cilium/cilium.go +++ b/pkg/cilium/cilium.go @@ -333,10 +333,6 @@ func RegisterCiliumTools(s *server.MCPServer) { // -- Debug Tools -- -func getCiliumPodName(nodeName string) (string, error) { - return getCiliumPodNameWithContext(context.Background(), nodeName) -} - func getCiliumPodNameWithContext(ctx context.Context, nodeName string) (string, error) { args := []string{"get", "pod", "-l", "k8s-app=cilium", "-o", "name", "-n", "kube-system"} if nodeName != "" { @@ -1032,27 +1028,6 @@ func handleGetServiceInformation(ctx context.Context, request mcp.CallToolReques return mcp.NewToolResultText(output), nil } -func handleDeleteService(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - serviceID := mcp.ParseString(request, "service_id", "") - all := mcp.ParseString(request, "all", "") == "true" - nodeName := mcp.ParseString(request, "node_name", "") - - var cmd string - if all { - cmd = "service delete --all" - } else if serviceID != "" { - cmd = fmt.Sprintf("service delete %s", serviceID) - } else { - return mcp.NewToolResultError("either service_id or all=true must be provided"), nil - } - - output, err := runCiliumDbgCommand(cmd, nodeName) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete service: %v", err)), nil - } - return mcp.NewToolResultText(output), nil -} - func handleUpdateService(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { backendWeights := mcp.ParseString(request, "backend_weights", "") backends := mcp.ParseString(request, "backends", "") diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index ee80a32e..d9661028 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -19,7 +19,6 @@ import ( "github.com/mark3labs/mcp-go/server" "github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms/openai" - v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -69,97 +68,6 @@ func NewK8sTool(llmModel llms.Model) (*K8sTool, error) { return &K8sTool{client: client, llmModel: llmModel}, nil } -func (k *K8sTool) getPodsNative(ctx context.Context, name, namespace string, allNamespaces bool, output string) (*mcp.CallToolResult, error) { - var pods *corev1.PodList - var err error - - if name != "" { - pod, err := k.client.clientset.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get pod: %v", err)), nil - } - pods = &corev1.PodList{Items: []corev1.Pod{*pod}} - } else if allNamespaces { - pods, err = k.client.clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{}) - } else { - pods, err = k.client.clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) - } - - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list pods: %v", err)), nil - } - - return formatResourceOutput(pods, output) -} - -func (k *K8sTool) getServicesNative(ctx context.Context, name, namespace string, allNamespaces bool, output string) (*mcp.CallToolResult, error) { - var services *corev1.ServiceList - var err error - - if name != "" { - service, err := k.client.clientset.CoreV1().Services(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get service: %v", err)), nil - } - services = &corev1.ServiceList{Items: []corev1.Service{*service}} - } else if allNamespaces { - services, err = k.client.clientset.CoreV1().Services("").List(ctx, metav1.ListOptions{}) - } else { - services, err = k.client.clientset.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{}) - } - - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list services: %v", err)), nil - } - - return formatResourceOutput(services, output) -} - -func (k *K8sTool) getDeploymentsNative(ctx context.Context, name, namespace string, allNamespaces bool, output string) (*mcp.CallToolResult, error) { - var deployments *v1.DeploymentList - var err error - - if name != "" { - deployment, err := k.client.clientset.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get deployment: %v", err)), nil - } - deployments = &v1.DeploymentList{Items: []v1.Deployment{*deployment}} - } else if allNamespaces { - deployments, err = k.client.clientset.AppsV1().Deployments("").List(ctx, metav1.ListOptions{}) - } else { - deployments, err = k.client.clientset.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{}) - } - - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list deployments: %v", err)), nil - } - - return formatResourceOutput(deployments, output) -} - -func (k *K8sTool) getConfigMapsNative(ctx context.Context, name, namespace string, allNamespaces bool, output string) (*mcp.CallToolResult, error) { - var configMaps *corev1.ConfigMapList - var err error - - if name != "" { - configMap, err := k.client.clientset.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get configmap: %v", err)), nil - } - configMaps = &corev1.ConfigMapList{Items: []corev1.ConfigMap{*configMap}} - } else if allNamespaces { - configMaps, err = k.client.clientset.CoreV1().ConfigMaps("").List(ctx, metav1.ListOptions{}) - } else { - configMaps, err = k.client.clientset.CoreV1().ConfigMaps(namespace).List(ctx, metav1.ListOptions{}) - } - - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list configmaps: %v", err)), nil - } - - return formatResourceOutput(configMaps, output) -} func formatResourceOutput(data interface{}, output string) (*mcp.CallToolResult, error) { if output == "json" || output == "" { @@ -321,7 +229,12 @@ func (k *K8sTool) handleCheckServiceConnectivity(ctx context.Context, request mc // This is a complex operation to perform natively, involving creating a temporary pod. // We'll keep the kubectl approach for this tool for now. podName := fmt.Sprintf("curl-test-%d", rand.Intn(10000)) - defer k.runKubectlCommand(ctx, []string{"delete", "pod", podName, "-n", namespace, "--ignore-not-found"}) + defer func() { + if _, err := k.runKubectlCommand(ctx, []string{"delete", "pod", podName, "-n", namespace, "--ignore-not-found"}); err != nil { + // Log the error but don't fail the operation + fmt.Printf("Warning: Failed to cleanup pod %s: %v\n", podName, err) + } + }() _, err := k.runKubectlCommand(ctx, []string{"run", podName, "--image=curlimages/curl", "-n", namespace, "--restart=Never", "--", "sleep", "3600"}) if err != nil { diff --git a/pkg/prometheus/prometheus_test.go b/pkg/prometheus/prometheus_test.go index d0246ac3..7b1b4c03 100644 --- a/pkg/prometheus/prometheus_test.go +++ b/pkg/prometheus/prometheus_test.go @@ -1,27 +1 @@ package prometheus - -import ( - "net/http" -) - -// mockRoundTripper is used to mock HTTP responses for testing -type mockRoundTripper struct { - response *http.Response - err error -} - -func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - if m.err != nil { - return nil, m.err - } - return m.response, nil -} - -func newTestClient(response *http.Response, err error) *http.Client { - return &http.Client{ - Transport: &mockRoundTripper{ - response: response, - err: err, - }, - } -} diff --git a/pkg/utils/datetime_test.go b/pkg/utils/datetime_test.go index 6b542599..8f1cd641 100644 --- a/pkg/utils/datetime_test.go +++ b/pkg/utils/datetime_test.go @@ -24,7 +24,7 @@ func TestHandleGetCurrentDateTimeTool(t *testing.T) { t.Fatal("Expected non-nil result") } - if result.Content == nil || len(result.Content) == 0 { + if len(result.Content) == 0 { t.Fatal("Expected content in result") } From f5fc0e137182e957f92ddd3c931144e6fd21eda6 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 18:37:53 +0200 Subject: [PATCH 05/41] fix tag flow Signed-off-by: Dmytro Rashko --- .github/workflows/tag.yaml | 8 ++++++-- Makefile | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index 6a479157..2f5349a0 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -33,9 +33,13 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + with: + platforms: linux/amd64,linux/arm64 + version: v0.23.0 + use: 'true' - name: 'Build Images' env: + BUILDX_BUILDER_NAME: kagent-builder-v0.23.0 DOCKER_BUILD_ARGS: "--push --platform linux/amd64,linux/arm64" DOCKER_BUILDER: "docker buildx" run: | @@ -66,7 +70,7 @@ jobs: else export VERSION=$(echo "$GITHUB_REF" | cut -c12-) fi - make build + make docker-build - name: Release uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') diff --git a/Makefile b/Makefile index c75a1843..4946eedc 100644 --- a/Makefile +++ b/Makefile @@ -117,7 +117,7 @@ buildx-create: .PHONY: docker-build # build tools image docker-build: fmt buildx-create - $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f Dockerfile ./ + $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -t $(TOOLS_IMG) -f Dockerfile ./ .PHONY: docker-build # build tools image for amd64 and arm64 docker-build-all: fmt buildx-create From 77f051cc8ef182fbedf5292d72402690a21de67b Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 18:55:55 +0200 Subject: [PATCH 06/41] fix tag flow Signed-off-by: Dmytro Rashko --- .github/workflows/tag.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index 2f5349a0..025642bc 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -70,7 +70,7 @@ jobs: else export VERSION=$(echo "$GITHUB_REF" | cut -c12-) fi - make docker-build + make build - name: Release uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') From b3ad3c5b6cd011678821f9b49070551744b7d454 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 19:01:04 +0200 Subject: [PATCH 07/41] fix tag flow Signed-off-by: Dmytro Rashko --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 4946eedc..2bde22b3 100644 --- a/Makefile +++ b/Makefile @@ -77,6 +77,9 @@ bin/kagent-tools-windows-amd64.exe.sha256: bin/kagent-tools-windows-amd64.exe .PHONY: build build: $(LOCALBIN) tidy fmt lint bin/kagent-tools-linux-amd64.sha256 bin/kagent-tools-linux-arm64.sha256 bin/kagent-tools-darwin-amd64.sha256 bin/kagent-tools-darwin-arm64.sha256 bin/kagent-tools-windows-amd64.exe.sha256 +build: + @echo "Build complete. Binaries are available in the bin/ directory." + ls -lt bin/kagent-tools-* TOOLS_IMAGE_NAME ?= tools TOOLS_IMAGE_TAG ?= $(VERSION) From af721a02f6cbb2a054c5a6d95dff6c86caed3aba Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 19:13:27 +0200 Subject: [PATCH 08/41] fix tag flow Signed-off-by: Dmytro Rashko --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2bde22b3..222bcf7d 100644 --- a/Makefile +++ b/Makefile @@ -76,7 +76,7 @@ bin/kagent-tools-windows-amd64.exe.sha256: bin/kagent-tools-windows-amd64.exe sha256sum bin/kagent-tools-windows-amd64.exe > bin/kagent-tools-windows-amd64.exe.sha256 .PHONY: build -build: $(LOCALBIN) tidy fmt lint bin/kagent-tools-linux-amd64.sha256 bin/kagent-tools-linux-arm64.sha256 bin/kagent-tools-darwin-amd64.sha256 bin/kagent-tools-darwin-arm64.sha256 bin/kagent-tools-windows-amd64.exe.sha256 +build: $(LOCALBIN) bin/kagent-tools-linux-amd64.sha256 bin/kagent-tools-linux-arm64.sha256 bin/kagent-tools-darwin-amd64.sha256 bin/kagent-tools-darwin-arm64.sha256 bin/kagent-tools-windows-amd64.exe.sha256 build: @echo "Build complete. Binaries are available in the bin/ directory." ls -lt bin/kagent-tools-* From 91ee83f7c7acef4811231b837af40d5a0ffa50d1 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 20:00:47 +0200 Subject: [PATCH 09/41] added LICENSE files Signed-off-by: Dmytro Rashko --- .devcontainer/Dockerfile | 11 + .devcontainer/devcontainer.json | 47 ++++ CODEOWNERS | 1 + CONTRIBUTION.md | 122 +++++++++ DEVELOPMENT.md | 441 ++++++++++++++++++++++++++++++++ LICENSE | 201 +++++++++++++++ README.md | 22 ++ SECURITY.md | 40 +++ 8 files changed, 885 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTION.md create mode 100644 DEVELOPMENT.md create mode 100644 LICENSE create mode 100644 SECURITY.md diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..c2500fec --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,11 @@ +### STAGE 1: download-tools-cli +ARG BASE_IMAGE_REGISTRY=cgr.dev +FROM $BASE_IMAGE_REGISTRY/chainguard/wolfi-base:latest AS tools + +RUN --mount=type=cache,target=/var/cache/apk,rw \ + echo "Installing on $BUILDPLATFORM" \ + && apk update \ + && apk add curl zsh go \ + && update-ca-certificates + +ENTRYPOINT ["zsh"] \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..b29037c2 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,47 @@ +{ + "name": "kagent-tools-container", + "build": { + "dockerfile": "Dockerfile", + "args": { + "TOOLS_GO_VERSION": "1.24.4", + } + }, + "customizations": { + "vscode": { + "extensions": [ + "golang.go", + "redhat.vscode-yaml", + "ms-kubernetes-tools.vscode-kubernetes-tools", + "ms-kubernetes-tools.kind-vscode", + "dbaeumer.vscode-eslint", + "ms-azuretools.vscode-docker", + "ms-vscode.makefile-tools", + "ms-vscode.vscode-go", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-toolsai.jupyter", + "ms-vscode.makefile-tools", + "ms-remote.remote-containers", + "ms-vscode.vscode-typescript-next", + "ms-azuretools.vscode-containers", + "ms-windows-ai-studio.windows-ai-studio", + "GitHub.copilot", + "GitHub.copilot-chat", + "Catppuccin.catppuccin-vsc", + "Catppuccin.catppuccin-vsc-icons" + ] + } + }, + + //user settings + "remoteUser": "root", + + //forward the following ports + //"forwardPorts": [8084], + + //mount docker directly on the host + "mounts": ["source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"], + + // Uncomment the next line to run commands after the container is created. + "postCreateCommand": "echo 'Container is ready!'" +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..4515c6a4 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +/* @EItanya @dimetron diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md new file mode 100644 index 00000000..a90f91ac --- /dev/null +++ b/CONTRIBUTION.md @@ -0,0 +1,122 @@ +# Contribution Guidelines + +## Development + +### Code of Conduct + +We are committed to providing a friendly, safe, and welcoming environment for all contributors. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md). + +### Getting Started + +1. **Fork the repository** on GitHub. +2. **Clone your fork** locally: + ```bash + git clone https://github.com/YOUR-USERNAME/tools.git + cd kagent + ``` +3. **Add the upstream repository** as a remote: + ```bash + git remote add upstream https://github.com/kagent-dev/tools.git + ``` +4. **Create a new branch** for your changes: + ```bash + git checkout -b feature/your-feature-name + ``` + +### Development Environment Setup + +See the [DEVELOPMENT.md](DEVELOPMENT.md) file for more information. + +### Making Changes + +#### Coding Standards + +- **Go Code**: + - Follow the [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) + - Run `make lint` before submitting your changes + - Ensure all tests pass with `make test` + - Add tests for new functionality + +#### Commit Guidelines + +We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: + +- **feat**: A new feature +- **fix**: A bug fix +- **docs**: Documentation only changes +- **style**: Changes that do not affect the meaning of the code +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **perf**: A code change that improves performance +- **test**: Adding missing tests or correcting existing tests +- **chore**: Changes to the build process or auxiliary tools + +Example commit message: +``` +feat(controller): add support for custom resource validation + +This adds validation for the KagentApp custom resource to ensure +that the configuration is valid before applying it to the cluster. + +Closes #123 +``` + +### Pull Request Process + +1. **Update your fork** with the latest changes from upstream: + ```bash + git fetch upstream + git rebase upstream/main + ``` + +2. **Push your changes** to your fork: + ```bash + git push origin feature/your-feature-name + ``` + +3. **Create a Pull Request** from your fork to the main repository. + +4. **Fill out the PR template** with all required information. + +5. **Address review comments** if requested by maintainers. + +6. **Update your PR** if needed: + ```bash + git add . + git commit -m "address review comments" + git push origin feature/your-feature-name + ``` + +7. Once approved, a maintainer will merge your PR. + + +### Documentation + +- Update documentation for any changes to APIs, CLIs, or user-facing features +- Add examples for new features +- Update the README if necessary +- Add comments to your code explaining complex logic + +### Releasing + +Only project maintainers can create releases. The process is: + +1. Update version numbers in relevant files +2. Create a release branch +3. Create a tag for the release +4. Build and publish artifacts +5. Create a GitHub release with release notes + +### Community + +- Join our [Discord server](https://discord.gg/Fu3k65f2k3) for discussions +- Participate in community calls (scheduled on our website) +- Help answer questions in GitHub issues +- Review pull requests from other contributors + +## License + +By contributing to this project, you agree that your contributions will be licensed under the project's license. + +## Questions? + +If you have any questions about contributing, please open an issue or reach out to the maintainers. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000..60cb9c6c --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,441 @@ +# Development Guide + +This document provides comprehensive development guidelines for the KAgent Tools Go project. + +## Prerequisites + +### Required Tools + +- **Go 1.24+** - Primary development language +- **Docker** - For containerization and testing +- **Make** - Build automation +- **Git** - Version control + +### Optional External Tools + +These tools enhance functionality but aren't required for basic development: + +- `kubectl` - Kubernetes CLI for k8s tools +- `helm` - Helm package manager for helm tools +- `istioctl` - Istio service mesh CLI for istio tools +- `cilium` - Cilium CLI for cilium tools + +## Project Structure + +``` +. +├── cmd/ +│ └── main.go # Entry point and MCP server +├── internal/ +│ └── version/ # Version and build metadata +├── pkg/ +│ ├── k8s/ # Kubernetes tools +│ ├── helm/ # Helm package manager tools +│ ├── istio/ # Istio service mesh tools +│ ├── cilium/ # Cilium CNI tools +│ ├── argo/ # Argo Rollouts tools +│ ├── prometheus/ # Prometheus monitoring tools +│ ├── utils/ # Common utilities +│ └── logger/ # Structured logging +├── tests/ # Integration tests +├── Makefile # Build automation +├── go.mod # Go module definition +└── go.sum # Go module checksums +``` + +## Development Workflow + +### 1. Environment Setup + +```bash +# Clone the repository +git clone +cd kagent-tools + +# Install dependencies +go mod download + +# Verify setup +go version +make help +``` + +### 2. Local Development + +```bash +# Build the project +make build + +# Run tests +make test + +# Run with verbose output +make test-verbose + +# Format code +make fmt + +# Lint code +make lint + +# Fix linting issues +make lint-fix +``` + +### 3. Running the Server + +```bash +# Build and run +make build +./bin/kagent-tools + +# Or run directly +go run ./cmd +``` + +The server starts an MCP server using SSE (Server-Sent Events) transport. + +## Code Quality Standards + +### Linting and Formatting + +```bash +# Format all Go files +go fmt ./... +make fmt + +# Run comprehensive linting +make lint + +# Fix auto-fixable lint issues +make lint-fix + +# Run go vet +make vet + +# Security vulnerability check +make govulncheck +``` + +### Testing Requirements + +- **Minimum 80% test coverage** - Use `go test -cover` to verify +- **Unit tests** for all public functions +- **Integration tests** for complex workflows +- **Table-driven tests** for multiple scenarios +- **Mock external dependencies** appropriately + +```bash +# Run tests with coverage +go test -v -cover ./... + +# Generate coverage report +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out + +# Run specific package tests +go test -v ./pkg/k8s +``` + +### Code Organization + +- **Single responsibility** - Each package has one clear purpose +- **Interface segregation** - Keep interfaces small and focused +- **Dependency injection** - Use interfaces for testability +- **Error handling** - Always handle errors explicitly +- **Context usage** - Use `context.Context` for cancellation + +## Architecture Guidelines + +### Tool Implementation Pattern + +Each tool category follows this pattern: + +```go +// pkg/[category]/[category].go +package category + +import ( + "context" + "github.com/mark3labs/mcp-go/pkg/mcp" +) + +type Tools struct { + // dependencies +} + +func NewTools() *Tools { + return &Tools{} +} + +func (t *Tools) RegisterTools(server *mcp.Server) { + server.RegisterTool("tool_name", t.handleTool) +} + +func (t *Tools) handleTool(ctx context.Context, params map[string]interface{}) (*mcp.ToolResult, error) { + // implementation +} +``` + +### Error Handling Standards + +```go +// Good: Wrap errors with context +if err != nil { + return nil, fmt.Errorf("failed to execute kubectl command: %w", err) +} + +// Good: Use custom error types when appropriate +type ValidationError struct { + Field string + Value interface{} +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("invalid %s: %v", e.Field, e.Value) +} +``` + +### Logging Standards + +```go +import "github.com/go-logr/logr" + +// Use structured logging +logger := logr.FromContextOrDiscard(ctx) +logger.Info("executing command", "command", cmd, "args", args) +logger.Error(err, "command failed", "command", cmd) +``` + +## Testing Guidelines + +### Unit Testing + +```go +func TestToolFunction(t *testing.T) { + tests := []struct { + name string + input interface{} + expected interface{} + wantErr bool + }{ + { + name: "valid input", + input: validInput, + expected: expectedOutput, + wantErr: false, + }, + { + name: "invalid input", + input: invalidInput, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ToolFunction(tt.input) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} +``` + +### Integration Testing + +```go +func TestIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + // Setup test environment + ctx := context.Background() + tools := NewTools() + + // Test actual functionality + result, err := tools.ExecuteCommand(ctx, "kubectl", []string{"version"}) + assert.NoError(t, err) + assert.Contains(t, result, "Client Version") +} +``` + +## Build and Deployment + +### Local Building + +```bash +# Build for current platform +make build + +# Build for all platforms +make build-all + +# Build specific platform +make bin/kagent-tools-linux-amd64 +``` + +### Docker + +```bash +# Build Docker image +make docker-build + +# Build with specific version +VERSION=v1.0.0 make docker-build + +# Run in Docker +docker run --rm kagent-tools:latest +``` + +### Release Process + +```bash +# Tag version +git tag v1.0.0 + +# Build release artifacts +make release + +# Push Docker image +make docker-push +``` + +## Environment Configuration + +### Environment Variables + +The application respects these environment variables: + +- `KUBECONFIG` - Kubernetes configuration file path +- `PROMETHEUS_URL` - Prometheus server URL +- `GRAFANA_URL` - Grafana server URL +- `GRAFANA_API_KEY` - Grafana API authentication key +- `LOG_LEVEL` - Logging level (debug, info, warn, error) + +### Configuration Files + +- `go.mod` - Go module dependencies +- `Makefile` - Build automation +- `.golangci.yml` - Linting configuration +- `Dockerfile` - Container build instructions + +## Debugging + +### Local Debugging + +```bash +# Run with debug logging +LOG_LEVEL=debug go run ./cmd + +# Use delve debugger +dlv debug ./cmd + +# Profile the application +go tool pprof http://localhost:6060/debug/pprof/profile +``` + +### Docker Debugging + +```bash +# Run container with debug shell +docker run -it --entrypoint /bin/sh kagent-tools:latest + +# Check container logs +docker logs +``` + +## Performance Considerations + +### Optimization Guidelines + +- **Avoid unnecessary allocations** in hot paths +- **Use connection pooling** for external services +- **Implement caching** for expensive operations +- **Use context timeouts** for external calls +- **Profile regularly** to identify bottlenecks + +### Memory Management + +```go +// Good: Reuse slices when possible +var buf []byte +if cap(buf) < needed { + buf = make([]byte, needed) +} +buf = buf[:needed] + +// Good: Use sync.Pool for expensive objects +var pool = sync.Pool{ + New: func() interface{} { + return &ExpensiveObject{} + }, +} +``` + +## Security Guidelines + +### Input Validation + +```go +// Always validate inputs +func validateInput(input string) error { + if input == "" { + return errors.New("input cannot be empty") + } + if len(input) > maxLength { + return errors.New("input too long") + } + return nil +} +``` + +### Secure Defaults + +- Use HTTPS for all external communications +- Validate all user inputs +- Handle sensitive data appropriately +- Keep dependencies updated +- Use secure random number generation + +## Contributing + +### Code Review Checklist + +- [ ] Code follows Go best practices +- [ ] Tests are included and passing +- [ ] Code coverage meets minimum requirements +- [ ] Linting passes without errors +- [ ] Documentation is updated +- [ ] Security considerations addressed +- [ ] Performance impact considered + +### Commit Guidelines + +```bash +# Format: (): +git commit -m "feat(k8s): add pod scaling functionality" +git commit -m "fix(helm): handle missing repository error" +git commit -m "docs(readme): update installation instructions" +``` + +## Troubleshooting + +### Common Issues + +1. **Build failures**: Check Go version and dependencies +2. **Test failures**: Verify external tool availability +3. **Linting errors**: Run `make lint-fix` for auto-fixes +4. **Import errors**: Run `go mod tidy` to clean dependencies + +### Getting Help + +- Check existing issues in the repository +- Review the CLAUDE.md file for project-specific guidance +- Consult Go documentation and best practices +- Ask questions in code reviews or team discussions \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f7b5e615 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Solo.io, Inc. + + 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/README.md b/README.md index 3139f0d3..56ab7c62 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ + + +--- + # KAgent Tools - Go Implementation This directory contains the Go implementation of all KAgent tools, migrated from the original Python implementation. The tools are designed to work with the Model Context Protocol (MCP) server and provide comprehensive Kubernetes, cloud-native, and observability functionality. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..a8d71206 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,40 @@ +## Security vulnerabilities + +Review how the kagent project handles the lifecycle of Common Vulnerability and Exposures (CVEs). + +### 📨 Where to report + +To report a security vulnerability, email the private Google group kagent-vulnerability-reports@googlegroups.com. + +### ✅ When to send a report + +Send a report when: + + You discover that a kagent component has a potential security vulnerability. + You are unsure whether or how a vulnerability affects kagent. + +### 🔔 Check before sending + +If in doubt, send a private message about potential vulnerabilities such as: + + Any crash, especially in kagent. + Any potential Denial of Service (DoS) attack. + +### ❌ When NOT to send a report + +Do not send a report for vulnerabilities that are not part of the kagent project, such as: + + You want help configuring kagent components for security purposes. + You want help applying security related updates to your kagent configuration or environment. + Your issue is not related to security vulnerabilities. + Your issue is related to base image dependencies, such as AutoGen. + +### Evaluation + +The kagent team evaluates vulnerability reports for: + + Severity level, which can affect the priority of the fix + Impact of the vulnerability on kagent code as opposed to backend code + Potential dependencies on third-party or backend code that might delay the remediation process + +The kagent team strives to keep private any vulnerability information with us as part of the remediation process. We only share information on a need-to-know basis to address the issue. From 430603cc5df12763a12fcd631e4e72ce7bd0c044 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 20:10:47 +0200 Subject: [PATCH 10/41] devcontainer Signed-off-by: Dmytro Rashko --- .devcontainer/Dockerfile | 24 ++++++++++++++++-------- .devcontainer/devcontainer.json | 2 -- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c2500fec..34780718 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,11 +1,19 @@ -### STAGE 1: download-tools-cli -ARG BASE_IMAGE_REGISTRY=cgr.dev -FROM $BASE_IMAGE_REGISTRY/chainguard/wolfi-base:latest AS tools +FROM mcr.microsoft.com/devcontainers/go:1-1.24-bookworm -RUN --mount=type=cache,target=/var/cache/apk,rw \ - echo "Installing on $BUILDPLATFORM" \ - && apk update \ - && apk add curl zsh go \ - && update-ca-certificates +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + gnupg \ + gnupg2 \ + htop \ + jq \ + less \ + lsof \ + net-tools \ + openssh-client \ + unzip \ + vim \ + zsh ENTRYPOINT ["zsh"] \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b29037c2..3816668f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -42,6 +42,4 @@ //mount docker directly on the host "mounts": ["source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"], - // Uncomment the next line to run commands after the container is created. - "postCreateCommand": "echo 'Container is ready!'" } From 1f62fc008f3d4b254b4d705922672c4b60ac3f64 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 20:57:48 +0200 Subject: [PATCH 11/41] added TOOLS_CILIUM_VERSION Signed-off-by: Dmytro Rashko --- .devcontainer/Dockerfile | 45 ++++++++++++++++++++++++++++++++- .devcontainer/devcontainer.json | 13 ++++++++-- Dockerfile | 1 + Makefile | 2 ++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 34780718..f3be754e 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,5 @@ -FROM mcr.microsoft.com/devcontainers/go:1-1.24-bookworm +ARG TOOLS_GO_VERSION +FROM mcr.microsoft.com/devcontainers/go:1-${TOOLS_GO_VERSION}-bookworm RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ @@ -16,4 +17,46 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ vim \ zsh +ARG TARGETARCH +ARG TOOLS_HELM_VERSION +ARG TOOLS_ISTIO_VERSION +ARG TOOLS_ARGO_ROLLOUTS_VERSION +ARG TOOLS_KUBECTL_VERSION +ARG TOOLS_CILIUM_VERSION + +WORKDIR /downloads + +RUN curl -LO "https://dl.k8s.io/release/v$TOOLS_KUBECTL_VERSION/bin/linux/$TARGETARCH/kubectl" \ + && mv kubectl /usr/bin/kubectl \ + && chmod +x /usr/bin/kubectl \ + && /usr/bin/kubectl version --client + +RUN curl -L https://istio.io/downloadIstio | ISTIO_VERSION=$TOOLS_ISTIO_VERSION TARGET_ARCH=$TARGETARCH sh - \ + && mv istio-*/bin/istioctl /usr/bin/ \ + && rm -rf istio-* \ + && /usr/bin/istioctl --help + +# Install Helm +RUN curl -Lo helm.tar.gz https://get.helm.sh/helm-v${TOOLS_HELM_VERSION}-linux-${TARGETARCH}.tar.gz \ + && tar -xvf helm.tar.gz \ + && mv linux-${TARGETARCH}/helm /usr/bin/helm \ + && chmod +x /usr/bin/helm \ + && rm -rf helm.tar.gz linux-${TARGETARCH} \ + && /usr/bin/helm version + +# Install kubectl-argo-rollouts +RUN curl -Lo /usr/bin/kubectl-argo-rollouts https://github.com/argoproj/argo-rollouts/releases/download/v${TOOLS_ARGO_ROLLOUTS_VERSION}/kubectl-argo-rollouts-linux-${TARGETARCH} \ + && chmod +x /usr/bin/kubectl-argo-rollouts \ + && /usr/bin/kubectl-argo-rollouts version + +# Install Cilium CLI +RUN curl -Lo cilium.tar.gz https://github.com/cilium/cilium-cli/releases/download/v${TOOLS_CILIUM_VERSION}/cilium-linux-${TARGETARCH}.tar.gz \ + && tar -xvf cilium.tar.gz \ + && mv cilium /usr/bin/cilium \ + && chmod +x /usr/bin/cilium \ + && rm -rf cilium.tar.gz \ + && /usr/bin/cilium version + +WORKDIR /tools + ENTRYPOINT ["zsh"] \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3816668f..91da9b7a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,9 +3,18 @@ "build": { "dockerfile": "Dockerfile", "args": { - "TOOLS_GO_VERSION": "1.24.4", + "TOOLS_GO_VERSION": "1.24", + "TOOLS_HELM_VERSION": "3.18.3", + "TOOLS_ISTIO_VERSION": "1.26.2", + "TOOLS_KUBECTL_VERSION": "1.33.2", + "TOOLS_ARGO_ROLLOUTS_VERSION": "1.8.3", + "TOOLS_CILIUM_VERSION": "0.16.29" } }, + "features": { + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, + "ghcr.io/mpriscella/features/kind:1": {} + }, "customizations": { "vscode": { "extensions": [ @@ -37,7 +46,7 @@ "remoteUser": "root", //forward the following ports - //"forwardPorts": [8084], + "forwardPorts": [8084], //mount docker directly on the host "mounts": ["source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"], diff --git a/Dockerfile b/Dockerfile index e307408a..ba459268 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ ARG TOOLS_HELM_VERSION ARG TOOLS_ISTIO_VERSION ARG TOOLS_ARGO_ROLLOUTS_VERSION ARG TOOLS_KUBECTL_VERSION +ARG TOOLS_CILIUM_VERSION WORKDIR /downloads diff --git a/Makefile b/Makefile index 222bcf7d..80abdee0 100644 --- a/Makefile +++ b/Makefile @@ -103,6 +103,7 @@ TOOLS_ISTIO_VERSION ?= 1.26.2 TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 TOOLS_KUBECTL_VERSION ?= 1.33.2 TOOLS_HELM_VERSION ?= 3.18.3 +TOOLS_CILIUM_VERSION ?= 1.17.5 # build args TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION) @@ -112,6 +113,7 @@ TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_ISTIO_VERSION=$(TOOLS_ISTIO_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_ARGO_ROLLOUTS_VERSION=$(TOOLS_ARGO_ROLLOUTS_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_KUBECTL_VERSION=$(TOOLS_KUBECTL_VERSION) TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_HELM_VERSION=$(TOOLS_HELM_VERSION) +TOOLS_IMAGE_BUILD_ARGS += --build-arg TOOLS_CILIUM_VERSION=$(TOOLS_CILIUM_VERSION) .PHONY: buildx-create buildx-create: From 9bfe7943d0db30e4db38598796cfa4108b19bacc Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 21:21:01 +0200 Subject: [PATCH 12/41] TOOLS_CILIUM_VERSION=0.18.5 Signed-off-by: Dmytro Rashko --- .devcontainer/devcontainer.json | 2 +- Dockerfile | 30 +++++++++++++++++++----------- Makefile | 2 +- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 91da9b7a..9b7a19c8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ "TOOLS_ISTIO_VERSION": "1.26.2", "TOOLS_KUBECTL_VERSION": "1.33.2", "TOOLS_ARGO_ROLLOUTS_VERSION": "1.8.3", - "TOOLS_CILIUM_VERSION": "0.16.29" + "TOOLS_CILIUM_VERSION": "0.18.5" } }, "features": { diff --git a/Dockerfile b/Dockerfile index ba459268..c8c4882c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,35 +10,42 @@ RUN apk update && apk add \ && rm -rf /var/cache/apk/* ARG TARGETARCH -ARG TOOLS_HELM_VERSION -ARG TOOLS_ISTIO_VERSION -ARG TOOLS_ARGO_ROLLOUTS_VERSION -ARG TOOLS_KUBECTL_VERSION -ARG TOOLS_CILIUM_VERSION - WORKDIR /downloads +ARG TOOLS_KUBECTL_VERSION RUN curl -LO "https://dl.k8s.io/release/v$TOOLS_KUBECTL_VERSION/bin/linux/$TARGETARCH/kubectl" \ && chmod +x kubectl \ && /downloads/kubectl version --client -RUN curl -L https://istio.io/downloadIstio | ISTIO_VERSION=$TOOLS_ISTIO_VERSION TARGET_ARCH=$TARGETARCH sh - \ - && mv istio-*/bin/istioctl /downloads/ \ - && rm -rf istio-* \ - && /downloads/istioctl --help - # Install Helm +ARG TOOLS_HELM_VERSION RUN curl -Lo helm.tar.gz https://get.helm.sh/helm-v${TOOLS_HELM_VERSION}-linux-${TARGETARCH}.tar.gz \ && tar -xvf helm.tar.gz \ && mv linux-${TARGETARCH}/helm /downloads/helm \ && chmod +x /downloads/helm \ && /downloads/helm version +ARG TOOLS_ISTIO_VERSION +RUN curl -L https://istio.io/downloadIstio | ISTIO_VERSION=$TOOLS_ISTIO_VERSION TARGET_ARCH=$TARGETARCH sh - \ + && mv istio-*/bin/istioctl /downloads/ \ + && rm -rf istio-* \ + && /downloads/istioctl --help + # Install kubectl-argo-rollouts +ARG TOOLS_ARGO_ROLLOUTS_VERSION RUN curl -Lo /downloads/kubectl-argo-rollouts https://github.com/argoproj/argo-rollouts/releases/download/v${TOOLS_ARGO_ROLLOUTS_VERSION}/kubectl-argo-rollouts-linux-${TARGETARCH} \ && chmod +x /downloads/kubectl-argo-rollouts \ && /downloads/kubectl-argo-rollouts version +# Install Cilium CLI +ARG TOOLS_CILIUM_VERSION +RUN curl -Lo cilium.tar.gz https://github.com/cilium/cilium-cli/releases/download/v${TOOLS_CILIUM_VERSION}/cilium-linux-${TARGETARCH}.tar.gz \ + && tar -xvf cilium.tar.gz \ + && mv cilium /downloads/cilium \ + && chmod +x /downloads/cilium \ + && rm -rf cilium.tar.gz \ + && /downloads/cilium version + ### STAGE 2: build-tools MCP ARG BASE_IMAGE_REGISTRY=cgr.dev ARG BUILDARCH=amd64 @@ -88,6 +95,7 @@ COPY --from=tools --chown=65532:65532 /downloads/kubectl /bin/kube COPY --from=tools --chown=65532:65532 /downloads/istioctl /bin/istioctl COPY --from=tools --chown=65532:65532 /downloads/helm /bin/helm COPY --from=tools --chown=65532:65532 /downloads/kubectl-argo-rollouts /bin/kubectl-argo-rollouts +COPY --from=tools --chown=65532:65532 /downloads/cilium /bin/cilium # Copy the tool-server binary COPY --from=builder --chown=65532:65532 /workspace/tool-server /tool-server diff --git a/Makefile b/Makefile index 80abdee0..71fa35d8 100644 --- a/Makefile +++ b/Makefile @@ -103,7 +103,7 @@ TOOLS_ISTIO_VERSION ?= 1.26.2 TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 TOOLS_KUBECTL_VERSION ?= 1.33.2 TOOLS_HELM_VERSION ?= 3.18.3 -TOOLS_CILIUM_VERSION ?= 1.17.5 +TOOLS_CILIUM_VERSION ?= 0.18.5 # build args TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION) From 2404e793386c5dd53bc91e7eef25822bc3d3d122 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 23:06:20 +0200 Subject: [PATCH 13/41] fix tools k8s Signed-off-by: Dmytro Rashko --- DEVELOPMENT.md | 18 +----------------- Makefile | 13 +++++++++++++ pkg/k8s/k8s.go | 2 -- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 60cb9c6c..4d9f5569 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -286,24 +286,8 @@ make bin/kagent-tools-linux-amd64 # Build Docker image make docker-build -# Build with specific version -VERSION=v1.0.0 make docker-build - # Run in Docker -docker run --rm kagent-tools:latest -``` - -### Release Process - -```bash -# Tag version -git tag v1.0.0 - -# Build release artifacts -make release - -# Push Docker image -make docker-push +make run ``` ## Environment Configuration diff --git a/Makefile b/Makefile index 71fa35d8..bc4ac1f9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ DOCKER_REGISTRY ?= ghcr.io BASE_IMAGE_REGISTRY ?= cgr.dev DOCKER_REPO ?= kagent-dev/kagent +KIND_CLUSTER_NAME ?= kagent BUILD_DATE := $(shell date -u '+%Y-%m-%d') GIT_COMMIT := $(shell git rev-parse --short HEAD || echo "unknown") @@ -81,6 +82,18 @@ build: @echo "Build complete. Binaries are available in the bin/ directory." ls -lt bin/kagent-tools-* +.PHONY: run +run: docker-build + @echo "Running tool server on http://localhost:8084/mcp ..." + @echo "Use: npx @modelcontextprotocol/inspector to connect to the tool server" + @docker run --rm -p 8084:8084 -e KAGENT_TOOLS_PORT=8084 $(TOOLS_IMG) + +PHONY: retag +retag: docker-build + @echo "Retagging tools image to $(RETAGGED_TOOLS_IMG)" + docker tag $(TOOLS_IMG) $(RETAGGED_TOOLS_IMG) + kind load docker-image --name $(KIND_CLUSTER_NAME) $(RETAGGED_TOOLS_IMG) + TOOLS_IMAGE_NAME ?= tools TOOLS_IMAGE_TAG ?= $(VERSION) TOOLS_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(TOOLS_IMAGE_NAME):$(TOOLS_IMAGE_TAG) diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index d9661028..056dcbd0 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -568,8 +568,6 @@ func RegisterK8sTools(s *server.MCPServer) { // Log the error and proceed without native tool implementations logger.Get().Info("Failed to initialize Kubernetes client, falling back to kubectl commands", "level", "warn", "error", err.Error()) - // Here you could register the pure-kubectl versions of the tools as a fallback - return } s.AddTool(mcp.NewTool("k8s_get_resources", mcp.WithDescription("Get Kubernetes resources using kubectl with enhanced native client support"), From caea1af033150332aac720bf14486fdf3f9a35b1 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 7 Jul 2025 23:42:28 +0200 Subject: [PATCH 14/41] tools local run Signed-off-by: Dmytro Rashko --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bc4ac1f9..90924db4 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ build: run: docker-build @echo "Running tool server on http://localhost:8084/mcp ..." @echo "Use: npx @modelcontextprotocol/inspector to connect to the tool server" - @docker run --rm -p 8084:8084 -e KAGENT_TOOLS_PORT=8084 $(TOOLS_IMG) + @docker run --rm --net=host -p 8084:8084 -e OPENAI_API_KEY=$(OPENAI_API_KEY) -v $(HOME)/.kube:/home/nonroot/.kube -e KAGENT_TOOLS_PORT=8084 $(TOOLS_IMG) -- --kubeconfig /root/.kube/config PHONY: retag retag: docker-build From b9faef9c55c0ef05f361ca25899399fb7318104c Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Tue, 8 Jul 2025 00:42:19 +0200 Subject: [PATCH 15/41] update tools with --kubeconfig Signed-off-by: Dmytro Rashko --- cmd/main.go | 41 +- go.mod | 49 +- go.sum | 108 ++- pkg/argo/argo.go | 27 +- pkg/cilium/cilium.go | 37 +- pkg/cilium/cilium_test.go | 314 ++------ pkg/helm/helm.go | 15 +- pkg/istio/istio.go | 16 +- pkg/k8s/k8s.go | 297 +++----- pkg/k8s/k8s_test.go | 1178 ++++++++++++++++++----------- pkg/prometheus/prometheus.go | 2 +- pkg/prometheus/prometheus_test.go | 508 +++++++++++++ pkg/utils/datetime.go | 9 +- 13 files changed, 1610 insertions(+), 991 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 2267733b..67222dff 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,6 +4,10 @@ import ( "context" "errors" "fmt" + "github.com/joho/godotenv" + "github.com/kagent-dev/tools/internal/version" + "github.com/kagent-dev/tools/pkg/logger" + "github.com/kagent-dev/tools/pkg/utils" "net/http" "os" "os/signal" @@ -12,11 +16,6 @@ import ( "syscall" "time" - "github.com/kagent-dev/tools/pkg/utils" - - "github.com/kagent-dev/tools/internal/version" - "github.com/kagent-dev/tools/pkg/logger" - "github.com/kagent-dev/tools/pkg/argo" "github.com/kagent-dev/tools/pkg/cilium" "github.com/kagent-dev/tools/pkg/helm" @@ -24,14 +23,14 @@ import ( "github.com/kagent-dev/tools/pkg/k8s" "github.com/kagent-dev/tools/pkg/prometheus" "github.com/mark3labs/mcp-go/server" - "github.com/mark3labs/mcp-go/util" "github.com/spf13/cobra" ) var ( - port int - stdio bool - tools []string + port int + stdio bool + tools []string + kubeconfig *string // These variables should be set during build time using -ldflags Name = "kagent-tools-server" @@ -50,6 +49,12 @@ func init() { rootCmd.Flags().IntVarP(&port, "port", "p", 8084, "Port to run the server on") rootCmd.Flags().BoolVar(&stdio, "stdio", false, "Use stdio for communication instead of HTTP") rootCmd.Flags().StringSliceVar(&tools, "tools", []string{}, "List of tools to register. If empty, all tools are registered.") + kubeconfig = rootCmd.Flags().String("kubeconfig", "", "kubeconfig file path (optional, defaults to in-cluster config)") + + // if found .env file, load it + if _, err := os.Stat(".env"); err == nil { + _ = godotenv.Load(".env") + } } func main() { @@ -75,7 +80,7 @@ func run(cmd *cobra.Command, args []string) { ) // Register tools - registerMCP(mcp, tools) + registerMCP(mcp, tools, *kubeconfig) // Create wait group for server goroutines var wg sync.WaitGroup @@ -85,7 +90,7 @@ func run(cmd *cobra.Command, args []string) { signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) // HTTP server reference (only used when not in stdio mode) - var sseServer *server.StreamableHTTPServer + var sseServer *server.SSEServer // Start server based on chosen mode wg.Add(1) @@ -95,7 +100,7 @@ func run(cmd *cobra.Command, args []string) { runStdioServer(ctx, mcp) }() } else { - sseServer = server.NewStreamableHTTPServer(mcp, server.WithLogger(util.DefaultLogger()), server.WithStateLess(true)) + sseServer = server.NewSSEServer(mcp) go func() { defer wg.Done() addr := fmt.Sprintf(":%d", port) @@ -142,9 +147,9 @@ func runStdioServer(ctx context.Context, mcp *server.MCPServer) { } } -func registerMCP(mcp *server.MCPServer, enabledToolProviders []string) { +func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string) { - var toolProviderMap = map[string]func(*server.MCPServer){ + var toolProviderMap = map[string]func(*server.MCPServer, string){ "utils": utils.RegisterDateTimeTools, "k8s": k8s.RegisterK8sTools, "prometheus": prometheus.RegisterPrometheusTools, @@ -154,12 +159,16 @@ func registerMCP(mcp *server.MCPServer, enabledToolProviders []string) { "cilium": cilium.RegisterCiliumTools, } + if len(kubeconfig) > 0 { + logger.Get().Info("Using kubeconfig file", "path", kubeconfig) + } + // If no tools specified, register all tools if len(enabledToolProviders) == 0 { logger.Get().Info("No specific tools provided, registering all tools") for toolProvider, registerFunc := range toolProviderMap { logger.Get().Info("Registering tools", "provider", toolProvider) - registerFunc(mcp) + registerFunc(mcp, kubeconfig) } return } @@ -169,7 +178,7 @@ func registerMCP(mcp *server.MCPServer, enabledToolProviders []string) { for _, toolProviderName := range enabledToolProviders { if registerFunc, ok := toolProviderMap[strings.ToLower(toolProviderName)]; ok { logger.Get().Info("Registering tool", "provider", toolProviderName) - registerFunc(mcp) + registerFunc(mcp, kubeconfig) } else { logger.Get().Error(nil, "Unknown tool specified", "provider", toolProviderName) } diff --git a/go.mod b/go.mod index fa06ede2..08a9e047 100644 --- a/go.mod +++ b/go.mod @@ -1,30 +1,28 @@ module github.com/kagent-dev/tools -go 1.24.1 +go 1.24.4 require ( github.com/go-logr/logr v1.4.3 github.com/go-logr/stdr v1.2.2 + github.com/joho/godotenv v1.5.1 + github.com/kagent-dev/kagent/go v0.0.0-20250707014726-aa7651a0e4e3 github.com/mark3labs/mcp-go v0.32.0 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 github.com/tmc/langchaingo v0.1.13 go.opentelemetry.io/otel v1.37.0 go.opentelemetry.io/otel/metric v1.37.0 - k8s.io/api v0.33.2 - k8s.io/apimachinery v0.33.2 - k8s.io/client-go v0.33.2 - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.10.0 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -32,32 +30,37 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.6 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/cast v1.7.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/spf13/cast v1.9.2 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.9.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/time v0.12.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.33.2 // indirect + k8s.io/apimachinery v0.33.2 // indirect + k8s.io/client-go v0.33.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index f2040cf5..15455ac7 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,27 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -34,27 +32,28 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kagent-dev/kagent/go v0.0.0-20250707014726-aa7651a0e4e3 h1:B5EkhSmYMG6bgn7DTsOfhal8sl1MmhjixSXP1PP/jNw= +github.com/kagent-dev/kagent/go v0.0.0-20250707014726-aa7651a0e4e3/go.mod h1:hwTH7K+UkePRxA6DhXOXavNyXRK3nPmvipA07DSRUxI= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8= github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -64,34 +63,30 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= @@ -110,6 +105,8 @@ go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/Wgbsd go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -119,38 +116,38 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -158,7 +155,6 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= @@ -169,12 +165,12 @@ k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a h1:ZV3Zr+/7s7aVbjNGICQt+ppKWsF1tehxggNfbM7XnG8= +k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= diff --git a/pkg/argo/argo.go b/pkg/argo/argo.go index 764e10b1..566a4a08 100644 --- a/pkg/argo/argo.go +++ b/pkg/argo/argo.go @@ -20,12 +20,14 @@ import ( // Argo Rollouts tools +var kubeConfig = "" + func handleVerifyArgoRolloutsControllerInstall(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { ns := mcp.ParseString(request, "namespace", "argo-rollouts") label := mcp.ParseString(request, "label", "app.kubernetes.io/component=rollouts-controller") cmd := []string{"get", "pods", "-n", ns, "-l", label, "-o", "jsonpath={.items[*].status.phase}"} - output, err := utils.RunCommandWithContext(ctx, "kubectl", cmd) + output, err := runArgoRolloutCommand(ctx, cmd) if err != nil { return mcp.NewToolResultError("Error: " + err.Error()), nil } @@ -60,7 +62,8 @@ func handleVerifyArgoRolloutsControllerInstall(ctx context.Context, request mcp. } func handleVerifyKubectlPluginInstall(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - output, err := utils.RunCommandWithContext(ctx, "kubectl", []string{"argo", "rollouts", "version"}) + args := []string{"argo", "rollouts", "version"} + output, err := runArgoRolloutCommand(ctx, args) if err != nil { return mcp.NewToolResultText("Kubectl Argo Rollouts plugin is not installed: " + err.Error()), nil } @@ -72,6 +75,13 @@ func handleVerifyKubectlPluginInstall(ctx context.Context, request mcp.CallToolR return mcp.NewToolResultText(output), nil } +func runArgoRolloutCommand(ctx context.Context, args []string) (string, error) { + if kubeConfig != "" { + args = append(args, "--kubeconfig", kubeConfig) + } + return utils.RunCommandWithContext(ctx, "kubectl", args) +} + func handlePromoteRollout(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { rolloutName := mcp.ParseString(request, "rollout_name", "") ns := mcp.ParseString(request, "namespace", "") @@ -91,7 +101,7 @@ func handlePromoteRollout(ctx context.Context, request mcp.CallToolRequest) (*mc cmd = append(cmd, "--full") } - output, err := utils.RunCommandWithContext(ctx, "kubectl", cmd) + output, err := runArgoRolloutCommand(ctx, cmd) if err != nil { return mcp.NewToolResultError("Error promoting rollout: " + err.Error()), nil } @@ -113,7 +123,7 @@ func handlePauseRollout(ctx context.Context, request mcp.CallToolRequest) (*mcp. } cmd = append(cmd, rolloutName) - output, err := utils.RunCommandWithContext(ctx, "kubectl", cmd) + output, err := runArgoRolloutCommand(ctx, cmd) if err != nil { return mcp.NewToolResultError("Error pausing rollout: " + err.Error()), nil } @@ -138,7 +148,7 @@ func handleSetRolloutImage(ctx context.Context, request mcp.CallToolRequest) (*m cmd = append(cmd, "-n", ns) } - output, err := utils.RunCommandWithContext(ctx, "kubectl", cmd) + output, err := runArgoRolloutCommand(ctx, cmd) if err != nil { return mcp.NewToolResultError("Error setting rollout image: " + err.Error()), nil } @@ -276,7 +286,7 @@ func handleVerifyGatewayPlugin(ctx context.Context, request mcp.CallToolRequest) // Check if ConfigMap exists and is configured cmd := []string{"get", "configmap", "argo-rollouts-config", "-n", namespace, "-o", "yaml"} - output, err := utils.RunCommandWithContext(ctx, "kubectl", cmd) + output, err := runArgoRolloutCommand(ctx, cmd) if err == nil && strings.Contains(output, "argoproj-labs/gatewayAPI") { status := GatewayPluginStatus{ Installed: true, @@ -304,7 +314,7 @@ func handleCheckPluginLogs(ctx context.Context, request mcp.CallToolRequest) (*m _ = mcp.ParseString(request, "timeout", "60") cmd := []string{"logs", "-n", namespace, "-l", "app.kubernetes.io/name=argo-rollouts", "--tail", "100"} - output, err := utils.RunCommandWithContext(ctx, "kubectl", cmd) + output, err := runArgoRolloutCommand(ctx, cmd) if err != nil { status := GatewayPluginStatus{ Installed: false, @@ -338,7 +348,8 @@ func handleCheckPluginLogs(ctx context.Context, request mcp.CallToolRequest) (*m return mcp.NewToolResultText(status.String()), nil } -func RegisterArgoTools(s *server.MCPServer) { +func RegisterArgoTools(s *server.MCPServer, kubeconfig string) { + kubeConfig = kubeconfig s.AddTool(mcp.NewTool("argo_verify_argo_rollouts_controller_install", mcp.WithDescription("Verify that the Argo Rollouts controller is installed and running"), mcp.WithString("namespace", mcp.Description("The namespace where Argo Rollouts is installed")), diff --git a/pkg/cilium/cilium.go b/pkg/cilium/cilium.go index de3cec99..a84cae32 100644 --- a/pkg/cilium/cilium.go +++ b/pkg/cilium/cilium.go @@ -11,7 +11,12 @@ import ( "github.com/mark3labs/mcp-go/server" ) +var kubeConfig = "" + func runCiliumCliWithContext(ctx context.Context, args ...string) (string, error) { + if kubeConfig != "" { + args = append([]string{"--kubeconfig", kubeConfig}, args...) + } return utils.RunCommandWithContext(ctx, "cilium", args) } @@ -195,7 +200,9 @@ func handleToggleClusterMesh(ctx context.Context, request mcp.CallToolRequest) ( return mcp.NewToolResultText(output), nil } -func RegisterCiliumTools(s *server.MCPServer) { +func RegisterCiliumTools(s *server.MCPServer, kubeconfig string) { + kubeConfig = kubeconfig + // Register debug tools RegisterCiliumDbgTools(s) @@ -329,6 +336,13 @@ func RegisterCiliumTools(s *server.MCPServer) { mcp.WithString("states", mcp.Description("The states to update the service with")), mcp.WithString("node_name", mcp.Description("The name of the node to update the service on")), ), handleUpdateService) + + s.AddTool(mcp.NewTool("cilium_delete_service", + mcp.WithDescription("Delete a service from the cluster"), + mcp.WithString("service_id", mcp.Description("The ID of the service to delete")), + mcp.WithString("all", mcp.Description("Whether to delete all services (true/false)")), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the service from")), + ), handleDeleteService) } // -- Debug Tools -- @@ -1028,6 +1042,27 @@ func handleGetServiceInformation(ctx context.Context, request mcp.CallToolReques return mcp.NewToolResultText(output), nil } +func handleDeleteService(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + serviceID := mcp.ParseString(request, "service_id", "") + all := mcp.ParseString(request, "all", "") == "true" + nodeName := mcp.ParseString(request, "node_name", "") + + var cmd string + if all { + cmd = "service delete --all" + } else if serviceID != "" { + cmd = fmt.Sprintf("service delete %s", serviceID) + } else { + return mcp.NewToolResultError("either service_id or all=true must be provided"), nil + } + + output, err := runCiliumDbgCommand(cmd, nodeName) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to delete service: %v", err)), nil + } + return mcp.NewToolResultText(output), nil +} + func handleUpdateService(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { backendWeights := mcp.ParseString(request, "backend_weights", "") backends := mcp.ParseString(request, "backends", "") diff --git a/pkg/cilium/cilium_test.go b/pkg/cilium/cilium_test.go index 22a90881..5b01846e 100644 --- a/pkg/cilium/cilium_test.go +++ b/pkg/cilium/cilium_test.go @@ -1,281 +1,99 @@ package cilium import ( - "strings" "testing" + + "github.com/stretchr/testify/assert" ) -func TestCiliumStatusAndVersion(t *testing.T) { - // Test cilium status argument construction - testCases := []struct { - namespace string - verbose bool - wait bool - expectedArgs []string - }{ - { - expectedArgs: []string{"status"}, - }, - { - namespace: "kube-system", - expectedArgs: []string{"status", "-n", "kube-system"}, - }, - { - verbose: true, - expectedArgs: []string{"status", "-v"}, - }, - { - wait: true, - expectedArgs: []string{"status", "--wait"}, - }, - { - namespace: "cilium-system", - verbose: true, - wait: true, - expectedArgs: []string{"status", "-n", "cilium-system", "-v", "--wait"}, - }, - } +// Basic command construction tests for Cilium CLI commands +// Note: MCP handler tests are in cilium_mcp_test.go - for i, tc := range testCases { +func TestCiliumCommandConstruction(t *testing.T) { + t.Run("basic command construction patterns", func(t *testing.T) { + // Test that we can construct basic cilium commands args := []string{"status"} + assert.Equal(t, "status", args[0]) - if tc.namespace != "" { - args = append(args, "-n", tc.namespace) + // Test upgrade command with parameters + upgradeArgs := []string{"upgrade"} + if clusterName := "test-cluster"; clusterName != "" { + upgradeArgs = append(upgradeArgs, "--cluster-name", clusterName) } - - if tc.verbose { - args = append(args, "-v") + if datapathMode := "tunnel"; datapathMode != "" { + upgradeArgs = append(upgradeArgs, "--datapath-mode", datapathMode) } - if tc.wait { - args = append(args, "--wait") - } + expected := []string{"upgrade", "--cluster-name", "test-cluster", "--datapath-mode", "tunnel"} + assert.Equal(t, expected, upgradeArgs) + }) - if len(args) != len(tc.expectedArgs) { - t.Errorf("Test case %d: expected %d args, got %d", i, len(tc.expectedArgs), len(args)) - continue + t.Run("install command with parameters", func(t *testing.T) { + args := []string{"install"} + if clusterName := "test-cluster"; clusterName != "" { + args = append(args, "--set", "cluster.name="+clusterName) } - - for j, arg := range args { - if arg != tc.expectedArgs[j] { - t.Errorf("Test case %d: expected arg %d to be '%s', got '%s'", i, j, tc.expectedArgs[j], arg) - } + if clusterID := "123"; clusterID != "" { + args = append(args, "--set", "cluster.id="+clusterID) } - } -} - -func TestCiliumConnectivity(t *testing.T) { - // Test cilium connectivity argument construction - testCases := []struct { - action string - namespace string - test string - expectedLength int - }{ - { - action: "test", - expectedLength: 2, // ["connectivity", "test"] - }, - { - action: "test", - namespace: "default", - expectedLength: 4, // ["connectivity", "test", "-n", "default"] - }, - { - action: "test", - test: "pod-to-pod", - expectedLength: 4, // ["connectivity", "test", "--test", "pod-to-pod"] - }, - } - - for i, tc := range testCases { - args := []string{"connectivity", tc.action} - - if tc.namespace != "" { - args = append(args, "-n", tc.namespace) + if datapathMode := "tunnel"; datapathMode != "" { + args = append(args, "--datapath-mode", datapathMode) } - if tc.test != "" { - args = append(args, "--test", tc.test) - } - - if len(args) != tc.expectedLength { - t.Errorf("Test case %d: expected %d args, got %d. Args: %v", i, tc.expectedLength, len(args), args) - } - } -} + expected := []string{"install", "--set", "cluster.name=test-cluster", "--set", "cluster.id=123", "--datapath-mode", "tunnel"} + assert.Equal(t, expected, args) + }) -func TestCiliumEndpoint(t *testing.T) { - // Test cilium endpoint argument construction - testCases := []struct { - action string - endpointID string - namespace string - expectedLength int - }{ - { - action: "list", - expectedLength: 2, // ["endpoint", "list"] - }, - { - action: "get", - endpointID: "1234", - expectedLength: 3, // ["endpoint", "get", "1234"] - }, - { - action: "list", - namespace: "default", - expectedLength: 4, // ["endpoint", "list", "-n", "default"] - }, - } + t.Run("clustermesh connect command", func(t *testing.T) { + clusterName := "remote-cluster" + context := "remote-context" - for i, tc := range testCases { - args := []string{"endpoint", tc.action} - - if tc.endpointID != "" { - args = append(args, tc.endpointID) + args := []string{"clustermesh", "connect", "--destination-cluster", clusterName} + if context != "" { + args = append(args, "--destination-context", context) } - if tc.namespace != "" { - args = append(args, "-n", tc.namespace) - } + expected := []string{"clustermesh", "connect", "--destination-cluster", "remote-cluster", "--destination-context", "remote-context"} + assert.Equal(t, expected, args) + }) - if len(args) != tc.expectedLength { - t.Errorf("Test case %d: expected %d args, got %d. Args: %v", i, tc.expectedLength, len(args), args) - } - } -} - -func TestCiliumPolicy(t *testing.T) { - // Test cilium policy argument construction - testCases := []struct { - action string - policyFile string - namespace string - expectedLength int - }{ - { - action: "get", - expectedLength: 2, // ["policy", "get"] - }, - { - action: "import", - policyFile: "policy.yaml", - expectedLength: 3, // ["policy", "import", "policy.yaml"] - }, - { - action: "get", - namespace: "default", - expectedLength: 4, // ["policy", "get", "-n", "default"] - }, - } - - for i, tc := range testCases { - args := []string{"policy", tc.action} - - if tc.policyFile != "" { - args = append(args, tc.policyFile) - } - - if tc.namespace != "" { - args = append(args, "-n", tc.namespace) - } + t.Run("bgp commands", func(t *testing.T) { + peersArgs := []string{"bgp", "peers"} + routesArgs := []string{"bgp", "routes"} - if len(args) != tc.expectedLength { - t.Errorf("Test case %d: expected %d args, got %d. Args: %v", i, tc.expectedLength, len(args), args) - } - } + assert.Equal(t, []string{"bgp", "peers"}, peersArgs) + assert.Equal(t, []string{"bgp", "routes"}, routesArgs) + }) } -func TestCiliumNode(t *testing.T) { - // Test cilium node argument construction - testCases := []struct { - action string - nodeName string - expectedLength int - }{ - { - action: "list", - expectedLength: 2, // ["node", "list"] - }, - { - action: "get", - nodeName: "node-1", - expectedLength: 3, // ["node", "get", "node-1"] - }, - } - - for i, tc := range testCases { - args := []string{"node", tc.action} - - if tc.nodeName != "" { - args = append(args, tc.nodeName) +func TestCiliumParameterValidation(t *testing.T) { + t.Run("cluster name validation", func(t *testing.T) { + clusterName := "" + if clusterName == "" { + assert.True(t, true, "cluster_name parameter should be required for connect operations") } - if len(args) != tc.expectedLength { - t.Errorf("Test case %d: expected %d args, got %d. Args: %v", i, tc.expectedLength, len(args), args) + clusterName = "valid-cluster" + if clusterName != "" { + assert.True(t, true, "valid cluster name should be accepted") } - } -} + }) -func TestCiliumDbgCommandConstruction(t *testing.T) { - // Test cilium-dbg command construction - testCases := []struct { - command string - nodeName string - expected string - }{ - { - command: "endpoint list", - expected: "endpoint list", - }, - { - command: "identity get 1234", - expected: "identity get 1234", - }, - { - command: "service list", - expected: "service list", - }, - } - - for _, tc := range testCases { - // This tests the command construction logic - cmdParts := strings.Fields(tc.command) - reconstructed := strings.Join(cmdParts, " ") - if reconstructed != tc.expected { - t.Errorf("Command reconstruction failed: expected '%s', got '%s'", tc.expected, reconstructed) - } - } -} + t.Run("boolean parameter handling", func(t *testing.T) { + enableStr := "true" + enable := enableStr == "true" + assert.True(t, enable) -func TestCiliumDbgParameterParsing(t *testing.T) { - // Test parameter parsing for cilium-dbg commands - testCases := []struct { - paramName string - paramValue string - expected string - }{ - { - paramName: "endpoint_id", - paramValue: "1234", - expected: "1234", - }, - { - paramName: "labels", - paramValue: "app=test", - expected: "app=test", - }, - { - paramName: "output_format", - paramValue: "json", - expected: "json", - }, - } + enableStr = "false" + enable = enableStr == "true" + assert.False(t, enable) - for _, tc := range testCases { - if tc.paramValue != tc.expected { - t.Errorf("Parameter parsing failed for %s: expected '%s', got '%s'", tc.paramName, tc.expected, tc.paramValue) + // Default value handling + enableStr = "" + if enableStr == "" { + enableStr = "true" // default } - } + enable = enableStr == "true" + assert.True(t, enable) + }) } diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 3c767d96..b3a65e3e 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -10,6 +10,8 @@ import ( "github.com/mark3labs/mcp-go/server" ) +var kubeConfig = "" // Global variable to hold kubeconfig path + // Helm list releases func handleHelmListReleases(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { namespace := mcp.ParseString(request, "namespace", "") @@ -65,7 +67,7 @@ func handleHelmListReleases(ctx context.Context, request mcp.CallToolRequest) (* args = append(args, "-o", output) } - result, err := utils.RunCommandWithContext(ctx, "helm", args) + result, err := runHelmCommand(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Helm list command failed: %v", err)), nil } @@ -73,6 +75,13 @@ func handleHelmListReleases(ctx context.Context, request mcp.CallToolRequest) (* return mcp.NewToolResultText(result), nil } +func runHelmCommand(ctx context.Context, args []string) (string, error) { + if kubeConfig != "" { + args = append(args, "--kubeconfig", kubeConfig) + } + return utils.RunCommandWithContext(ctx, "helm", args) +} + // Helm get release func handleHelmGetRelease(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { name := mcp.ParseString(request, "name", "") @@ -216,7 +225,9 @@ func handleHelmRepoUpdate(ctx context.Context, request mcp.CallToolRequest) (*mc } // Register Helm tools -func RegisterHelmTools(s *server.MCPServer) { +func RegisterHelmTools(s *server.MCPServer, kubeconfig string) { + kubeConfig = kubeconfig + s.AddTool(mcp.NewTool("helm_list_releases", mcp.WithDescription("List Helm releases in a namespace"), mcp.WithString("namespace", mcp.Description("The namespace to list releases from")), diff --git a/pkg/istio/istio.go b/pkg/istio/istio.go index 2c7b68cc..2f198aac 100644 --- a/pkg/istio/istio.go +++ b/pkg/istio/istio.go @@ -10,6 +10,8 @@ import ( "github.com/mark3labs/mcp-go/server" ) +var kubeConfig = "" // Global variable to hold kubeconfig path + // Istio proxy status func handleIstioProxyStatus(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { podName := mcp.ParseString(request, "pod_name", "") @@ -25,7 +27,7 @@ func handleIstioProxyStatus(ctx context.Context, request mcp.CallToolRequest) (* args = append(args, podName) } - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl proxy-status failed: %v", err)), nil } @@ -33,6 +35,14 @@ func handleIstioProxyStatus(ctx context.Context, request mcp.CallToolRequest) (* return mcp.NewToolResultText(result), nil } +func runIstioCtl(ctx context.Context, args []string) (string, error) { + if kubeConfig != "" { + args = append(args, "--kubeconfig", kubeConfig) + } + result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + return result, err +} + // Istio proxy config func handleIstioProxyConfig(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { podName := mcp.ParseString(request, "pod_name", "") @@ -288,7 +298,9 @@ func handleZtunnelConfig(ctx context.Context, request mcp.CallToolRequest) (*mcp } // Register Istio tools -func RegisterIstioTools(s *server.MCPServer) { +func RegisterIstioTools(s *server.MCPServer, kubeconfig string) { + kubeConfig = kubeconfig + // Istio proxy status s.AddTool(mcp.NewTool("istio_proxy_status", mcp.WithDescription("Get Envoy proxy status for pods, retrieves last sent and acknowledged xDS sync from Istiod to each Envoy in the mesh"), diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index 056dcbd0..6bd73ec9 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -3,7 +3,6 @@ package k8s import ( "context" _ "embed" - "encoding/json" "fmt" "maps" "math/rand" @@ -11,79 +10,62 @@ import ( "slices" "strings" - "k8s.io/client-go/tools/clientcmd" - "github.com/kagent-dev/tools/pkg/logger" "github.com/kagent-dev/tools/pkg/utils" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms/openai" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" ) -// K8sClient wraps Kubernetes client operations -type K8sClient struct { - clientset kubernetes.Interface - config *rest.Config +// K8sTool struct to hold the LLM model +type K8sTool struct { + kubeconfig string + llmModel llms.Model } -// NewK8sClient creates a new Kubernetes client -func NewK8sClient() (*K8sClient, error) { - config, err := rest.InClusterConfig() - if err != nil { - // Fallback to kubeconfig - config, err = clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile) - if err != nil { - return nil, fmt.Errorf("failed to create k8s config: %v", err) - } - } - - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("failed to create k8s clientset: %v", err) - } - - return &K8sClient{ - clientset: clientset, - config: config, - }, nil +func NewK8sTool(llmModel llms.Model) *K8sTool { + return &K8sTool{llmModel: llmModel} } -// K8sTool struct to hold the client -type K8sTool struct { - client *K8sClient - llmModel llms.Model +func NewK8sToolWithConfig(kubeconfig string, llmModel llms.Model) *K8sTool { + return &K8sTool{kubeconfig: kubeconfig, llmModel: llmModel} } -func NewK8sTool(llmModel llms.Model) (*K8sTool, error) { - client, err := NewK8sClient() - if err != nil { - return nil, err +// Enhanced kubectl get +func (k *K8sTool) handleKubectlGetEnhanced(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + resourceType := mcp.ParseString(request, "resource_type", "") + resourceName := mcp.ParseString(request, "resource_name", "") + namespace := mcp.ParseString(request, "namespace", "") + allNamespaces := mcp.ParseString(request, "all_namespaces", "") == "true" + output := mcp.ParseString(request, "output", "json") + + if resourceType == "" { + return mcp.NewToolResultError("resource_type parameter is required"), nil } - return &K8sTool{client: client, llmModel: llmModel}, nil -} + args := []string{"get", resourceType} -func formatResourceOutput(data interface{}, output string) (*mcp.CallToolResult, error) { - if output == "json" || output == "" { - jsonData, err := json.MarshalIndent(data, "", " ") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal JSON: %v", err)), nil - } - return mcp.NewToolResultText(string(jsonData)), nil + if resourceName != "" { + args = append(args, resourceName) + } + + if allNamespaces { + args = append(args, "--all-namespaces") + } else if namespace != "" { + args = append(args, "-n", namespace) + } + + if output != "" { + args = append(args, "-o", output) + } else { + args = append(args, "-o", "json") } - // For other output formats, convert to string representation - jsonData, _ := json.Marshal(data) - return mcp.NewToolResultText(string(jsonData)), nil + return k.runKubectlCommand(ctx, args) } -// Enhanced get pod logs with native client +// Get pod logs func (k *K8sTool) handleKubectlLogsEnhanced(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { podName := mcp.ParseString(request, "pod_name", "") namespace := mcp.ParseString(request, "namespace", "default") @@ -94,24 +76,20 @@ func (k *K8sTool) handleKubectlLogsEnhanced(ctx context.Context, request mcp.Cal return mcp.NewToolResultError("pod_name parameter is required"), nil } - lines := int64(tailLines) - logOptions := &corev1.PodLogOptions{ - TailLines: &lines, - } + args := []string{"logs", podName, "-n", namespace} if container != "" { - logOptions.Container = container + args = append(args, "-c", container) } - logs, err := k.client.clientset.CoreV1().Pods(namespace).GetLogs(podName, logOptions).DoRaw(ctx) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get pod logs: %v", err)), nil + if tailLines > 0 { + args = append(args, "--tail", fmt.Sprintf("%d", tailLines)) } - return mcp.NewToolResultText(string(logs)), nil + return k.runKubectlCommand(ctx, args) } -// Scale deployment using native client +// Scale deployment func (k *K8sTool) handleScaleDeployment(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { deploymentName := mcp.ParseString(request, "name", "") namespace := mcp.ParseString(request, "namespace", "default") @@ -121,23 +99,12 @@ func (k *K8sTool) handleScaleDeployment(ctx context.Context, request mcp.CallToo return mcp.NewToolResultError("name parameter is required"), nil } - deployment, err := k.client.clientset.AppsV1().Deployments(namespace).Get(ctx, deploymentName, metav1.GetOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get deployment: %v", err)), nil - } - - replicasInt32 := int32(replicas) - deployment.Spec.Replicas = &replicasInt32 - - _, err = k.client.clientset.AppsV1().Deployments(namespace).Update(ctx, deployment, metav1.UpdateOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to scale deployment: %v", err)), nil - } + args := []string{"scale", "deployment", deploymentName, "--replicas", fmt.Sprintf("%d", replicas), "-n", namespace} - return mcp.NewToolResultText(fmt.Sprintf("Deployment %s scaled to %d replicas", deploymentName, replicas)), nil + return k.runKubectlCommand(ctx, args) } -// Patch resource using native client +// Patch resource func (k *K8sTool) handlePatchResource(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { resourceType := mcp.ParseString(request, "resource_type", "") resourceName := mcp.ParseString(request, "resource_name", "") @@ -148,12 +115,9 @@ func (k *K8sTool) handlePatchResource(ctx context.Context, request mcp.CallToolR return mcp.NewToolResultError("resource_type, resource_name, and patch parameters are required"), nil } - _, err := k.client.clientset.CoreV1().Pods(namespace).Patch(ctx, resourceName, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to patch resource: %v", err)), nil - } + args := []string{"patch", resourceType, resourceName, "-p", patch, "-n", namespace} - return mcp.NewToolResultText(fmt.Sprintf("Resource %s/%s patched successfully", resourceType, resourceName)), nil + return k.runKubectlCommand(ctx, args) } // Apply manifest from content @@ -164,9 +128,6 @@ func (k *K8sTool) handleApplyManifest(ctx context.Context, request mcp.CallToolR return mcp.NewToolResultError("manifest parameter is required"), nil } - // This handler still uses kubectl apply, which is not ideal for native Go implementation. - // For a pure Go approach, we would parse the manifest and use the appropriate client to create/update resources. - // This is a complex task and for now we will keep the kubectl fallback. tmpFile, err := os.CreateTemp("", "manifest-*.yaml") if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to create temp file: %v", err)), nil @@ -181,7 +142,7 @@ func (k *K8sTool) handleApplyManifest(ctx context.Context, request mcp.CallToolR return k.runKubectlCommand(ctx, []string{"apply", "-f", tmpFile.Name()}) } -// Delete resource using native client +// Delete resource func (k *K8sTool) handleDeleteResource(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { resourceType := mcp.ParseString(request, "resource_type", "") resourceName := mcp.ParseString(request, "resource_name", "") @@ -191,30 +152,9 @@ func (k *K8sTool) handleDeleteResource(ctx context.Context, request mcp.CallTool return mcp.NewToolResultError("resource_type and resource_name parameters are required"), nil } - deletePolicy := metav1.DeletePropagationForeground - deleteOptions := metav1.DeleteOptions{ - PropagationPolicy: &deletePolicy, - } - - var err error - switch resourceType { - case "pods", "pod": - err = k.client.clientset.CoreV1().Pods(namespace).Delete(ctx, resourceName, deleteOptions) - case "services", "service", "svc": - err = k.client.clientset.CoreV1().Services(namespace).Delete(ctx, resourceName, deleteOptions) - case "deployments", "deployment", "deploy": - err = k.client.clientset.AppsV1().Deployments(namespace).Delete(ctx, resourceName, deleteOptions) - case "configmaps", "configmap", "cm": - err = k.client.clientset.CoreV1().ConfigMaps(namespace).Delete(ctx, resourceName, deleteOptions) - default: - return mcp.NewToolResultError(fmt.Sprintf("Unsupported resource type for deletion: %s", resourceType)), nil - } - - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to delete resource: %v", err)), nil - } + args := []string{"delete", resourceType, resourceName, "-n", namespace} - return mcp.NewToolResultText(fmt.Sprintf("Resource %s/%s deleted successfully", resourceType, resourceName)), nil + return k.runKubectlCommand(ctx, args) } // Check service connectivity @@ -226,41 +166,43 @@ func (k *K8sTool) handleCheckServiceConnectivity(ctx context.Context, request mc return mcp.NewToolResultError("service_name parameter is required"), nil } - // This is a complex operation to perform natively, involving creating a temporary pod. - // We'll keep the kubectl approach for this tool for now. + // Create a temporary curl pod for connectivity check podName := fmt.Sprintf("curl-test-%d", rand.Intn(10000)) defer func() { - if _, err := k.runKubectlCommand(ctx, []string{"delete", "pod", podName, "-n", namespace, "--ignore-not-found"}); err != nil { - // Log the error but don't fail the operation - fmt.Printf("Warning: Failed to cleanup pod %s: %v\n", podName, err) - } + _, _ = k.runKubectlCommand(ctx, []string{"delete", "pod", podName, "-n", namespace, "--ignore-not-found"}) }() + // Create the curl pod _, err := k.runKubectlCommand(ctx, []string{"run", podName, "--image=curlimages/curl", "-n", namespace, "--restart=Never", "--", "sleep", "3600"}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to create curl pod: %v", err)), nil } + // Wait for pod to be ready _, err = k.runKubectlCommand(ctx, []string{"wait", "--for=condition=ready", "pod/" + podName, "-n", namespace, "--timeout=60s"}) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to wait for curl pod: %v", err)), nil } + // Execute curl command return k.runKubectlCommand(ctx, []string{"exec", podName, "-n", namespace, "--", "curl", "-s", serviceName}) } -// Get cluster events using native client +// Get cluster events func (k *K8sTool) handleGetEvents(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { namespace := mcp.ParseString(request, "namespace", "") - events, err := k.client.clientset.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get events: %v", err)), nil + args := []string{"get", "events", "-o", "json"} + if namespace != "" { + args = append(args, "-n", namespace) + } else { + args = append(args, "--all-namespaces") } - return formatResourceOutput(events, "json") + + return k.runKubectlCommand(ctx, args) } -// Execute command in pod using native client +// Execute command in pod func (k *K8sTool) handleExecCommand(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { podName := mcp.ParseString(request, "pod_name", "") namespace := mcp.ParseString(request, "namespace", "default") @@ -270,46 +212,17 @@ func (k *K8sTool) handleExecCommand(ctx context.Context, request mcp.CallToolReq return mcp.NewToolResultError("pod_name and command parameters are required"), nil } - // This handler uses kubectl exec. - return k.runKubectlCommand(ctx, []string{"exec", podName, "-n", namespace, "--", command}) -} - -// Fallback to kubectl command for get operations -func (k *K8sTool) handleKubectlGetTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - resourceType := mcp.ParseString(request, "resource_type", "") - resourceName := mcp.ParseString(request, "resource_name", "") - namespace := mcp.ParseString(request, "namespace", "") - output := mcp.ParseString(request, "output", "wide") - allNamespaces := mcp.ParseBoolean(request, "all_namespaces", false) - - if resourceType == "" { - return mcp.NewToolResultError("resource_type parameter is required"), nil - } - - args := []string{"get", resourceType} - - if resourceName != "" { - args = append(args, resourceName) - } - - if namespace != "" { - args = append(args, "-n", namespace) - } - - if allNamespaces { - args = append(args, "-A") - } - - if output != "" { - args = append(args, "-o", output) - } else { - args = append(args, "-o", "json") - } + args := []string{"exec", podName, "-n", namespace, "--", command} return k.runKubectlCommand(ctx, args) } -// Fallback to kubectl command for describe operations +// Get available API resources +func (k *K8sTool) handleGetAvailableAPIResources(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.runKubectlCommand(ctx, []string{"api-resources", "-o", "json"}) +} + +// Kubectl describe tool func (k *K8sTool) handleKubectlDescribeTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { resourceType := mcp.ParseString(request, "resource_type", "") resourceName := mcp.ParseString(request, "resource_name", "") @@ -327,29 +240,7 @@ func (k *K8sTool) handleKubectlDescribeTool(ctx context.Context, request mcp.Cal return k.runKubectlCommand(ctx, args) } -func (k *K8sTool) runKubectlCommand(ctx context.Context, args []string) (*mcp.CallToolResult, error) { - result, err := utils.RunCommandWithContext(ctx, "kubectl", args) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - return mcp.NewToolResultText(result), nil -} - -func (k *K8sTool) handleGetAvailableAPIResources(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - serverResources, err := k.client.clientset.Discovery().ServerPreferredResources() - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get available API resources: %v", err)), nil - } - - // We can format this into a more readable string or return the JSON - jsonData, err := json.MarshalIndent(serverResources, "", " ") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal JSON: %v", err)), nil - } - - return mcp.NewToolResultText(string(jsonData)), nil -} - +// Rollout operations func (k *K8sTool) handleRollout(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { action := mcp.ParseString(request, "action", "") resourceType := mcp.ParseString(request, "resource_type", "") @@ -368,10 +259,12 @@ func (k *K8sTool) handleRollout(ctx context.Context, request mcp.CallToolRequest return k.runKubectlCommand(ctx, args) } +// Get cluster configuration func (k *K8sTool) handleGetClusterConfiguration(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return k.runKubectlCommand(ctx, []string{"config", "view"}) } +// Remove annotation func (k *K8sTool) handleRemoveAnnotation(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { resourceType := mcp.ParseString(request, "resource_type", "") resourceName := mcp.ParseString(request, "resource_name", "") @@ -390,6 +283,7 @@ func (k *K8sTool) handleRemoveAnnotation(ctx context.Context, request mcp.CallTo return k.runKubectlCommand(ctx, args) } +// Remove label func (k *K8sTool) handleRemoveLabel(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { resourceType := mcp.ParseString(request, "resource_type", "") resourceName := mcp.ParseString(request, "resource_name", "") @@ -408,6 +302,7 @@ func (k *K8sTool) handleRemoveLabel(ctx context.Context, request mcp.CallToolReq return k.runKubectlCommand(ctx, args) } +// Annotate resource func (k *K8sTool) handleAnnotateResource(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { resourceType := mcp.ParseString(request, "resource_type", "") resourceName := mcp.ParseString(request, "resource_name", "") @@ -428,6 +323,7 @@ func (k *K8sTool) handleAnnotateResource(ctx context.Context, request mcp.CallTo return k.runKubectlCommand(ctx, args) } +// Label resource func (k *K8sTool) handleLabelResource(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { resourceType := mcp.ParseString(request, "resource_type", "") resourceName := mcp.ParseString(request, "resource_name", "") @@ -448,6 +344,7 @@ func (k *K8sTool) handleLabelResource(ctx context.Context, request mcp.CallToolR return k.runKubectlCommand(ctx, args) } +// Create resource from URL func (k *K8sTool) handleCreateResourceFromURL(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { url := mcp.ParseString(request, "url", "") namespace := mcp.ParseString(request, "namespace", "") @@ -464,6 +361,7 @@ func (k *K8sTool) handleCreateResourceFromURL(ctx context.Context, request mcp.C return k.runKubectlCommand(ctx, args) } +// Resource generation embeddings var ( //go:embed resources/istio/peer_auth.md istioAuthPolicy string @@ -507,6 +405,7 @@ var ( resourceTypes = maps.Keys(resourceMap) ) +// Generate resource using LLM func (k *K8sTool) handleGenerateResource(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { resourceType := mcp.ParseString(request, "resource_type", "") resourceDescription := mcp.ParseString(request, "resource_description", "") @@ -533,7 +432,6 @@ func (k *K8sTool) handleGenerateResource(ctx context.Context, request mcp.CallTo llms.TextContent{Text: systemPrompt}, }, }, - { Role: llms.ChatMessageTypeHuman, Parts: []llms.ContentPart{ @@ -555,7 +453,20 @@ func (k *K8sTool) handleGenerateResource(ctx context.Context, request mcp.CallTo return mcp.NewToolResultText(c1.Content), nil } -func RegisterK8sTools(s *server.MCPServer) { +// Helper function to run kubectl commands +func (k *K8sTool) runKubectlCommand(ctx context.Context, args []string) (*mcp.CallToolResult, error) { + if k.kubeconfig != "" { + args = append([]string{"--kubeconfig", k.kubeconfig}, args...) + } + result, err := utils.RunCommandWithContext(ctx, "kubectl", args) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + return mcp.NewToolResultText(result), nil +} + +// RegisterK8sTools registers all k8s tools with the MCP server +func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { var llm llms.Model if openAiClient, err := openai.New(); err == nil { llm = openAiClient @@ -563,23 +474,19 @@ func RegisterK8sTools(s *server.MCPServer) { logger.Get().Error(err, "Failed to initialize OpenAI LLM, k8s_generate_resource tool will not be available") } - k8sTool, err := NewK8sTool(llm) - if err != nil { - // Log the error and proceed without native tool implementations - logger.Get().Info("Failed to initialize Kubernetes client, falling back to kubectl commands", - "level", "warn", "error", err.Error()) - } + k8sTool := NewK8sToolWithConfig(kubeconfig, llm) + s.AddTool(mcp.NewTool("k8s_get_resources", - mcp.WithDescription("Get Kubernetes resources using kubectl with enhanced native client support"), + mcp.WithDescription("Get Kubernetes resources using kubectl"), mcp.WithString("resource_type", mcp.Description("Type of resource (pod, service, deployment, etc.)"), mcp.Required()), mcp.WithString("resource_name", mcp.Description("Name of specific resource (optional)")), mcp.WithString("namespace", mcp.Description("Namespace to query (optional)")), - mcp.WithBoolean("all_namespaces", mcp.Description("Query all namespaces (true/false)")), + mcp.WithString("all_namespaces", mcp.Description("Query all namespaces (true/false)")), mcp.WithString("output", mcp.Description("Output format (json, yaml, wide, etc.)")), - ), k8sTool.handleKubectlGetTool) + ), k8sTool.handleKubectlGetEnhanced) s.AddTool(mcp.NewTool("k8s_get_pod_logs", - mcp.WithDescription("Get logs from a Kubernetes pod with enhanced native client support"), + mcp.WithDescription("Get logs from a Kubernetes pod"), mcp.WithString("pod_name", mcp.Description("Name of the pod"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace of the pod (default: default)")), mcp.WithString("container", mcp.Description("Container name (for multi-container pods)")), @@ -587,7 +494,7 @@ func RegisterK8sTools(s *server.MCPServer) { ), k8sTool.handleKubectlLogsEnhanced) s.AddTool(mcp.NewTool("k8s_scale", - mcp.WithDescription("Scale a Kubernetes deployment using native client"), + mcp.WithDescription("Scale a Kubernetes deployment"), mcp.WithString("name", mcp.Description("Name of the deployment"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace of the deployment (default: default)")), mcp.WithNumber("replicas", mcp.Description("Number of replicas"), mcp.Required()), @@ -607,7 +514,7 @@ func RegisterK8sTools(s *server.MCPServer) { ), k8sTool.handleApplyManifest) s.AddTool(mcp.NewTool("k8s_delete_resource", - mcp.WithDescription("Delete a Kubernetes resource using native client"), + mcp.WithDescription("Delete a Kubernetes resource"), mcp.WithString("resource_type", mcp.Description("Type of resource (pod, service, deployment, etc.)"), mcp.Required()), mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), @@ -620,7 +527,7 @@ func RegisterK8sTools(s *server.MCPServer) { ), k8sTool.handleCheckServiceConnectivity) s.AddTool(mcp.NewTool("k8s_get_events", - mcp.WithDescription("Get Kubernetes cluster events using native client"), + mcp.WithDescription("Get Kubernetes cluster events"), mcp.WithString("namespace", mcp.Description("Namespace to query events from (optional, default: all namespaces)")), ), k8sTool.handleGetEvents) diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go index dae3215d..240e7aca 100644 --- a/pkg/k8s/k8s_test.go +++ b/pkg/k8s/k8s_test.go @@ -2,6 +2,8 @@ package k8s import ( "context" + "fmt" + "os" "testing" "github.com/kagent-dev/tools/pkg/utils" @@ -9,36 +11,16 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tmc/langchaingo/llms" - v1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/rest" - k8stesting "k8s.io/client-go/testing" - "k8s.io/utils/ptr" ) -// Helper function to create a test K8sTool with fake client -func newTestK8sTool(clientset kubernetes.Interface) *K8sTool { - return &K8sTool{ - client: &K8sClient{ - clientset: clientset, - config: &rest.Config{}, - }, - } +// Helper function to create a test K8sTool +func newTestK8sTool() *K8sTool { + return NewK8sTool(nil) } -// Helper function to create a test K8sTool with fake client and mock LLM -func newTestK8sToolWithLLM(clientset kubernetes.Interface, llm llms.Model) *K8sTool { - return &K8sTool{ - client: &K8sClient{ - clientset: clientset, - config: &rest.Config{}, - }, - llmModel: llm, - } +// Helper function to create a test K8sTool with mock LLM +func newTestK8sToolWithLLM(llm llms.Model) *K8sTool { + return NewK8sTool(llm) } // Helper function to extract text content from MCP result @@ -52,55 +34,16 @@ func getResultText(result *mcp.CallToolResult) string { return "" } -func TestNewK8sClient(t *testing.T) { - // Test that NewK8sClient handles errors gracefully - // This will likely fail in test environment without kubeconfig, which is expected - _, err := NewK8sClient() - // We don't fail the test if client creation fails, as it's expected in test env - if err != nil { - t.Logf("NewK8sClient failed as expected in test environment: %v", err) - } -} - -func TestFormatResourceOutput(t *testing.T) { - testData := map[string]interface{}{ - "test": "data", - "number": 42, - } - - // Test JSON output format - result, err := formatResourceOutput(testData, "json") - if err != nil { - t.Fatalf("formatResourceOutput failed: %v", err) - } - - if len(result.Content) == 0 { - t.Fatal("Expected non-empty content") - } - - // Test empty output format (defaults to JSON) - result, err = formatResourceOutput(testData, "") - if err != nil { - t.Fatalf("formatResourceOutput with empty format failed: %v", err) - } - - if len(result.Content) == 0 { - t.Fatal("Expected non-empty content") - } -} - func TestHandleGetAvailableAPIResources(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - - // Mock the discovery client - clientset.Fake.PrependReactor("get", "*", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { - return true, &corev1.PodList{}, nil - }) + mock := utils.NewMockShellExecutor() + expectedOutput := `[{"name": "pods", "singularName": "pod", "namespaced": true, "kind": "Pod"}]` + mock.AddCommandString("kubectl", []string{"api-resources", "-o", "json"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) - k8sTool := newTestK8sTool(clientset) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} result, err := k8sTool.handleGetAvailableAPIResources(ctx, req) @@ -110,21 +53,21 @@ func TestHandleGetAvailableAPIResources(t *testing.T) { // Check that we got some content assert.NotEmpty(t, result.Content) + assert.Contains(t, getResultText(result), "pods") }) - t.Run("error handling", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - clientset.Fake.PrependReactor("*", "*", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { - return true, nil, assert.AnError - }) + t.Run("kubectl command failure", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + mock.AddCommandString("kubectl", []string{"api-resources", "-o", "json"}, "", assert.AnError) + ctx := utils.WithShellExecutor(ctx, mock) - k8sTool := newTestK8sTool(clientset) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} result, err := k8sTool.handleGetAvailableAPIResources(ctx, req) assert.NoError(t, err) // MCP handlers should not return Go errors assert.NotNil(t, result) - // Should handle the error gracefully + assert.True(t, result.IsError) }) } @@ -132,18 +75,12 @@ func TestHandleScaleDeployment(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - deployment := &v1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-deployment", - Namespace: "default", - }, - Spec: v1.DeploymentSpec{ - Replicas: ptr.To(int32(3)), - }, - } - clientset := fake.NewSimpleClientset(deployment) + mock := utils.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment scaled` + mock.AddCommandString("kubectl", []string{"scale", "deployment", "test-deployment", "--replicas", "5", "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) - k8sTool := newTestK8sTool(clientset) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -158,22 +95,60 @@ func TestHandleScaleDeployment(t *testing.T) { resultText := getResultText(result) assert.Contains(t, resultText, "test-deployment") + assert.Contains(t, resultText, "scaled") }) - t.Run("missing parameters", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + t.Run("missing name parameter", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ - "name": "test-deployment", - // Missing replicas parameter + // Missing name parameter (this is the required one) + "replicas": float64(3), } result, err := k8sTool.handleScaleDeployment(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "name parameter is required") + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) + }) + + t.Run("missing replicas parameter uses default", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment scaled` + // Default replicas is 1 + mock.AddCommandString("kubectl", []string{"scale", "deployment", "test-deployment", "--replicas", "1", "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() + + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "name": "test-deployment", + // Missing replicas parameter - should use default value of 1 + } + + result, err := k8sTool.handleScaleDeployment(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "scaled") + + // Verify the command was executed with default replicas=1 + callLog := mock.GetCallLog() + assert.Len(t, callLog, 1) + assert.Equal(t, "kubectl", callLog[0].Command) + assert.Equal(t, []string{"scale", "deployment", "test-deployment", "--replicas", "1", "-n", "default"}, callLog[0].Args) }) } @@ -181,16 +156,12 @@ func TestHandleGetEvents(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - event := &corev1.Event{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-event", - Namespace: "default", - }, - Message: "Test event message", - } - clientset := fake.NewSimpleClientset(event) + mock := utils.NewMockShellExecutor() + expectedOutput := `{"items": [{"metadata": {"name": "test-event"}, "message": "Test event message"}]}` + mock.AddCommandString("kubectl", []string{"get", "events", "-o", "json", "--all-namespaces"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) - k8sTool := newTestK8sTool(clientset) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} result, err := k8sTool.handleGetEvents(ctx, req) @@ -203,8 +174,12 @@ func TestHandleGetEvents(t *testing.T) { }) t.Run("with namespace", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + mock := utils.NewMockShellExecutor() + expectedOutput := `{"items": []}` + mock.AddCommandString("kubectl", []string{"get", "events", "-o", "json", "-n", "custom-namespace"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -214,7 +189,7 @@ func TestHandleGetEvents(t *testing.T) { result, err := k8sTool.handleGetEvents(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) - // Should not error even if no events found + assert.False(t, result.IsError) }) } @@ -222,8 +197,10 @@ func TestHandlePatchResource(t *testing.T) { ctx := context.Background() t.Run("missing parameters", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -235,17 +212,19 @@ func TestHandlePatchResource(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) t.Run("valid parameters", func(t *testing.T) { - deployment := &v1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-deployment", - Namespace: "default", - }, - } - clientset := fake.NewSimpleClientset(deployment) - k8sTool := newTestK8sTool(clientset) + mock := utils.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment patched` + mock.AddCommandString("kubectl", []string{"patch", "deployment", "test-deployment", "-p", `{"spec":{"replicas":5}}`, "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -257,7 +236,10 @@ func TestHandlePatchResource(t *testing.T) { result, err := k8sTool.handlePatchResource(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) - // Should attempt to patch (may fail in test env but validates parameters) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "patched") }) } @@ -265,8 +247,10 @@ func TestHandleDeleteResource(t *testing.T) { ctx := context.Background() t.Run("missing parameters", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -278,17 +262,19 @@ func TestHandleDeleteResource(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) t.Run("valid parameters", func(t *testing.T) { - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-pod", - Namespace: "default", - }, - } - clientset := fake.NewSimpleClientset(pod) - k8sTool := newTestK8sTool(clientset) + mock := utils.NewMockShellExecutor() + expectedOutput := `pod "test-pod" deleted` + mock.AddCommandString("kubectl", []string{"delete", "pod", "test-pod", "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -299,7 +285,10 @@ func TestHandleDeleteResource(t *testing.T) { result, err := k8sTool.handleDeleteResource(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) - // Should attempt to delete (may succeed or fail depending on implementation) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "deleted") }) } @@ -307,8 +296,10 @@ func TestHandleCheckServiceConnectivity(t *testing.T) { ctx := context.Background() t.Run("missing service_name", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{} @@ -317,11 +308,24 @@ func TestHandleCheckServiceConnectivity(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) t.Run("valid service_name", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + mock := utils.NewMockShellExecutor() + + // Mock the pod creation, wait, and exec commands using partial matchers + mock.AddPartialMatcherString("kubectl", []string{"run", "*", "--image=curlimages/curl", "-n", "default", "--restart=Never", "--", "sleep", "3600"}, "pod/curl-test-123 created", nil) + mock.AddPartialMatcherString("kubectl", []string{"wait", "--for=condition=ready", "*", "-n", "default", "--timeout=60s"}, "pod/curl-test-123 condition met", nil) + mock.AddPartialMatcherString("kubectl", []string{"exec", "*", "-n", "default", "--", "curl", "-s", "test-service.default.svc.cluster.local:80"}, "Connection successful", nil) + mock.AddPartialMatcherString("kubectl", []string{"delete", "pod", "*", "-n", "default", "--ignore-not-found"}, "pod deleted", nil) + + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -331,7 +335,7 @@ func TestHandleCheckServiceConnectivity(t *testing.T) { result, err := k8sTool.handleCheckServiceConnectivity(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) - // Should attempt connectivity check (will likely fail in test env but validates params) + // Should attempt connectivity check (may succeed or fail but validates params) }) } @@ -339,8 +343,10 @@ func TestHandleKubectlDescribeTool(t *testing.T) { ctx := context.Background() t.Run("missing parameters", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -352,11 +358,21 @@ func TestHandleKubectlDescribeTool(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) t.Run("valid parameters", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + mock := utils.NewMockShellExecutor() + expectedOutput := `Name: test-deployment +Namespace: default +Labels: app=test` + mock.AddCommandString("kubectl", []string{"describe", "deployment", "test-deployment", "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -368,193 +384,81 @@ func TestHandleKubectlDescribeTool(t *testing.T) { result, err := k8sTool.handleKubectlDescribeTool(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) - // Should attempt to describe (may fail in test env but validates parameters) - }) -} - -func TestHandleGenerateResource(t *testing.T) { - ctx := context.Background() - - t.Run("missing parameters", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - mockLLM := newMockLLM(&llms.ContentResponse{}, nil) - k8sTool := newTestK8sToolWithLLM(clientset, mockLLM) - - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "resource_type": "istio_auth_policy", - // Missing resource_description - } - - result, err := k8sTool.handleGenerateResource(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "resource_type and resource_description parameters are required") - }) - - t.Run("valid istio auth policy generation", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - expectedResponse := `apiVersion: security.istio.io/v1beta1 -kind: PeerAuthentication -metadata: - name: test-auth-policy - namespace: default -spec: - selector: - matchLabels: - app: test-app - mtls: - mode: STRICT` - - mockLLM := newMockLLM(&llms.ContentResponse{ - Choices: []*llms.ContentChoice{ - {Content: expectedResponse}, - }, - }, nil) - k8sTool := newTestK8sToolWithLLM(clientset, mockLLM) - - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "resource_type": "istio_auth_policy", - "resource_description": "A peer authentication policy for strict mTLS", - } - - result, err := k8sTool.handleGenerateResource(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.False(t, result.IsError) - - resultText := getResultText(result) - assert.Equal(t, expectedResponse, resultText) - - // Verify the mock was called - assert.Equal(t, 1, mockLLM.called) - }) - - t.Run("valid gateway api gateway generation", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - expectedResponse := `apiVersion: gateway.networking.k8s.io/v1beta1 -kind: Gateway -metadata: - name: test-gateway - namespace: default -spec: - gatewayClassName: istio - listeners: - - name: http - port: 80` - - mockLLM := newMockLLM(&llms.ContentResponse{ - Choices: []*llms.ContentChoice{ - {Content: expectedResponse}, - }, - }, nil) - k8sTool := newTestK8sToolWithLLM(clientset, mockLLM) - - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "resource_type": "gateway_api_gateway", - "resource_description": "A gateway for HTTP traffic", - } - - result, err := k8sTool.handleGenerateResource(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) assert.False(t, result.IsError) resultText := getResultText(result) - assert.Equal(t, expectedResponse, resultText) - - // Verify the mock was called - assert.Equal(t, 1, mockLLM.called) + assert.Contains(t, resultText, "test-deployment") }) +} - t.Run("unsupported resource type", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - mockLLM := newMockLLM(&llms.ContentResponse{}, nil) - k8sTool := newTestK8sToolWithLLM(clientset, mockLLM) - - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "resource_type": "unsupported_resource_type", - "resource_description": "Some description", - } - - result, err := k8sTool.handleGenerateResource(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "resource type unsupported_resource_type not found") - - // Verify the mock was never called since validation failed - assert.Equal(t, 0, mockLLM.called) - }) +func TestHandleKubectlGetEnhanced(t *testing.T) { + ctx := context.Background() - t.Run("LLM generation error", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - mockLLM := newMockLLM(nil, assert.AnError) - k8sTool := newTestK8sToolWithLLM(clientset, mockLLM) + t.Run("missing resource_type", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "resource_type": "istio_auth_policy", - "resource_description": "A peer authentication policy for strict mTLS", - } - - result, err := k8sTool.handleGenerateResource(ctx, req) + result, err := k8sTool.handleKubectlGetEnhanced(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "failed to generate content") - // Verify the mock was called - assert.Equal(t, 1, mockLLM.called) + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) - t.Run("LLM empty response", func(t *testing.T) { - clientset := fake.NewSimpleClientset() - mockLLM := newMockLLM(&llms.ContentResponse{ - Choices: []*llms.ContentChoice{}, // Empty choices - }, nil) - k8sTool := newTestK8sToolWithLLM(clientset, mockLLM) + t.Run("valid resource_type", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `{"items": [{"metadata": {"name": "pod1"}}]}` + mock.AddCommandString("kubectl", []string{"get", "pods", "-o", "json"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "resource_type": "istio_auth_policy", - "resource_description": "A peer authentication policy for strict mTLS", - } - - result, err := k8sTool.handleGenerateResource(ctx, req) + req.Params.Arguments = map[string]interface{}{"resource_type": "pods"} + result, err := k8sTool.handleKubectlGetEnhanced(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "empty response from model") - - // Verify the mock was called - assert.Equal(t, 1, mockLLM.called) + assert.False(t, result.IsError) }) } func TestHandleKubectlLogsEnhanced(t *testing.T) { ctx := context.Background() - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) t.Run("missing pod_name", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} result, err := k8sTool.handleKubectlLogsEnhanced(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) t.Run("valid pod_name", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `log line 1 +log line 2` + mock.AddCommandString("kubectl", []string{"logs", "test-pod", "-n", "default", "--tail", "50"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{"pod_name": "test-pod"} result, err := k8sTool.handleKubectlLogsEnhanced(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) + assert.False(t, result.IsError) }) } @@ -575,8 +479,7 @@ spec: mock.AddPartialMatcherString("kubectl", []string{"apply", "-f", "*"}, expectedOutput, nil) ctx := utils.WithShellExecutor(context.Background(), mock) - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -607,8 +510,7 @@ spec: mock := utils.NewMockShellExecutor() ctx := utils.WithShellExecutor(context.Background(), mock) - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -638,8 +540,7 @@ drwxr-xr-x 1 root root 4096 Jan 1 12:00 ..` mock.AddCommandString("kubectl", []string{"exec", "mypod", "-n", "default", "--", "ls -la"}, expectedOutput, nil) ctx := utils.WithShellExecutor(context.Background(), mock) - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -668,8 +569,7 @@ drwxr-xr-x 1 root root 4096 Jan 1 12:00 ..` mock := utils.NewMockShellExecutor() ctx := utils.WithShellExecutor(context.Background(), mock) - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -683,7 +583,7 @@ drwxr-xr-x 1 root root 4096 Jan 1 12:00 ..` assert.True(t, result.IsError) assert.Contains(t, getResultText(result), "pod_name and command parameters are required") - // Verify no commands were executed + // Verify no commands were executed since parameters are missing callLog := mock.GetCallLog() assert.Len(t, callLog, 0) }) @@ -697,8 +597,7 @@ func TestHandleRollout(t *testing.T) { mock.AddCommandString("kubectl", []string{"rollout", "restart", "deployment/myapp", "-n", "default"}, expectedOutput, nil) ctx := utils.WithShellExecutor(context.Background(), mock) - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -724,46 +623,11 @@ func TestHandleRollout(t *testing.T) { assert.Equal(t, []string{"rollout", "restart", "deployment/myapp", "-n", "default"}, callLog[0].Args) }) - t.Run("rollout status check", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `deployment "myapp" successfully rolled out` - - mock.AddCommandString("kubectl", []string{"rollout", "status", "deployment/myapp", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) - - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "action": "status", - "resource_type": "deployment", - "resource_name": "myapp", - "namespace": "default", - } - - result, err := k8sTool.handleRollout(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.False(t, result.IsError) - - // Verify the expected output - content := getResultText(result) - assert.Contains(t, content, "successfully rolled out") - - // Verify the correct kubectl command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"rollout", "status", "deployment/myapp", "-n", "default"}, callLog[0].Args) - }) - t.Run("missing required parameters", func(t *testing.T) { mock := utils.NewMockShellExecutor() ctx := utils.WithShellExecutor(context.Background(), mock) - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ @@ -777,239 +641,679 @@ func TestHandleRollout(t *testing.T) { assert.True(t, result.IsError) assert.Contains(t, getResultText(result), "required") - // Verify no commands were executed + // Verify no commands were executed since parameters are missing callLog := mock.GetCallLog() assert.Len(t, callLog, 0) }) } -func TestHandleLabelResource(t *testing.T) { - ctx := context.Background() - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) +// Mock LLM for testing +type mockLLM struct { + called int + response *llms.ContentResponse + error error +} + +func newMockLLM(response *llms.ContentResponse, err error) *mockLLM { + return &mockLLM{ + response: response, + error: err, + } +} + +func (m *mockLLM) Call(ctx context.Context, prompt string, options ...llms.CallOption) (string, error) { + return "", nil +} + +func (m *mockLLM) GenerateContent(ctx context.Context, _ []llms.MessageContent, options ...llms.CallOption) (*llms.ContentResponse, error) { + m.called++ + return m.response, m.error +} + +func TestHandleGenerateResource(t *testing.T) { + ctx := context.Background() + + t.Run("success", func(t *testing.T) { + expectedYAML := `apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: default + namespace: foo +spec: + mtls: + mode: STRICT` + + mockLLM := newMockLLM(&llms.ContentResponse{ + Choices: []*llms.ContentChoice{ + {Content: expectedYAML}, + }, + }, nil) + + k8sTool := newTestK8sToolWithLLM(mockLLM) + + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "istio_auth_policy", + "resource_description": "A peer authentication policy for strict mTLS", + } + + result, err := k8sTool.handleGenerateResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "PeerAuthentication") + assert.Contains(t, resultText, "STRICT") + + // Verify the mock was called + assert.Equal(t, 1, mockLLM.called) + }) t.Run("missing parameters", func(t *testing.T) { + k8sTool := newTestK8sTool() + req := mcp.CallToolRequest{} - result, err := k8sTool.handleLabelResource(ctx, req) + req.Params.Arguments = map[string]interface{}{ + "resource_type": "istio_auth_policy", + // Missing resource_description + } + + result, err := k8sTool.handleGenerateResource(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "required") }) - t.Run("valid parameters", func(t *testing.T) { + t.Run("no LLM model", func(t *testing.T) { + k8sTool := newTestK8sTool() // No LLM model + req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{"resource_type": "pod", "resource_name": "test-pod", "labels": "app=test"} - result, err := k8sTool.handleLabelResource(ctx, req) + req.Params.Arguments = map[string]interface{}{ + "resource_type": "istio_auth_policy", + "resource_description": "A peer authentication policy for strict mTLS", + } + + result, err := k8sTool.handleGenerateResource(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "No LLM client present") + }) + + t.Run("invalid resource type", func(t *testing.T) { + mockLLM := newMockLLM(&llms.ContentResponse{ + Choices: []*llms.ContentChoice{ + {Content: "test"}, + }, + }, nil) + + k8sTool := newTestK8sToolWithLLM(mockLLM) + + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "invalid_resource_type", + "resource_description": "A test resource", + } + + result, err := k8sTool.handleGenerateResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "resource type invalid_resource_type not found") + + // Verify the mock was not called + assert.Equal(t, 0, mockLLM.called) }) } +// Test additional handlers that were missing tests func TestHandleAnnotateResource(t *testing.T) { ctx := context.Background() - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + + t.Run("success", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment annotated` + mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1=value1", "key2=value2", "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "annotations": "key1=value1 key2=value2", + "namespace": "default", + } + + result, err := k8sTool.handleAnnotateResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "annotated") + }) t.Run("missing parameters", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "deployment", + // Missing resource_name and annotations + } + result, err := k8sTool.handleAnnotateResource(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "required") + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) +} + +func TestHandleLabelResource(t *testing.T) { + ctx := context.Background() + + t.Run("success", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment labeled` + mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env=prod", "version=1.0", "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() - t.Run("valid parameters", func(t *testing.T) { req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{"resource_type": "pod", "resource_name": "test-pod", "annotations": "foo=bar"} - result, err := k8sTool.handleAnnotateResource(ctx, req) + req.Params.Arguments = map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "labels": "env=prod version=1.0", + "namespace": "default", + } + + result, err := k8sTool.handleLabelResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "labeled") + }) + + t.Run("missing parameters", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() + + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "deployment", + // Missing resource_name and labels + } + + result, err := k8sTool.handleLabelResource(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "required") + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) } func TestHandleRemoveAnnotation(t *testing.T) { ctx := context.Background() - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) - t.Run("missing parameters", func(t *testing.T) { + t.Run("success", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment annotated` + mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1-", "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "annotation_key": "key1", + "namespace": "default", + } + result, err := k8sTool.handleRemoveAnnotation(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) - assert.True(t, result.IsError) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "annotated") }) - t.Run("valid parameters", func(t *testing.T) { + t.Run("missing parameters", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() + req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{"resource_type": "pod", "resource_name": "test-pod", "annotation_key": "foo"} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "deployment", + // Missing resource_name and annotation_key + } + result, err := k8sTool.handleRemoveAnnotation(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "required") + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) } func TestHandleRemoveLabel(t *testing.T) { ctx := context.Background() - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) - t.Run("missing parameters", func(t *testing.T) { + t.Run("success", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment labeled` + mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env-", "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "label_key": "env", + "namespace": "default", + } + result, err := k8sTool.handleRemoveLabel(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) - assert.True(t, result.IsError) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "labeled") }) - t.Run("valid parameters", func(t *testing.T) { + t.Run("missing parameters", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() + req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{"resource_type": "pod", "resource_name": "test-pod", "label_key": "foo"} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "deployment", + // Missing resource_name and label_key + } + result, err := k8sTool.handleRemoveLabel(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "required") + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) } func TestHandleCreateResourceFromURL(t *testing.T) { ctx := context.Background() - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) - t.Run("missing url", func(t *testing.T) { + t.Run("success", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment created` + mock.AddCommandString("kubectl", []string{"create", "-f", "https://example.com/manifest.yaml", "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "url": "https://example.com/manifest.yaml", + "namespace": "default", + } + result, err := k8sTool.handleCreateResourceFromURL(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) - assert.True(t, result.IsError) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "created") }) - t.Run("valid url", func(t *testing.T) { + t.Run("missing url parameter", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() + req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{"url": "http://example.com/manifest.yaml"} + req.Params.Arguments = map[string]interface{}{ + // Missing url parameter + } + result, err := k8sTool.handleCreateResourceFromURL(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "url parameter is required") + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) } func TestHandleGetClusterConfiguration(t *testing.T) { ctx := context.Background() - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) - req := mcp.CallToolRequest{} - result, err := k8sTool.handleGetClusterConfiguration(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) + t.Run("success", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `apiVersion: v1 +clusters: +- cluster: + server: https://kubernetes.default.svc + name: default +contexts: +- context: + cluster: default + user: default + name: default +current-context: default +kind: Config +preferences: {} +users: +- name: default` + mock.AddCommandString("kubectl", []string{"config", "view"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + + req := mcp.CallToolRequest{} + result, err := k8sTool.handleGetClusterConfiguration(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "current-context") + assert.Contains(t, resultText, "clusters") + }) } -func TestHandleGetResourceYAML(t *testing.T) { +// Test the k8s_create_resource handler (inline function in RegisterK8sTools) +func TestHandleCreateResource(t *testing.T) { ctx := context.Background() - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) - // This handler is registered as an anonymous func, so we test the logic directly - // Simulate the parameters - resourceType := "pod" - resourceName := "test-pod" - namespace := "default" - - args := []string{"get", resourceType, resourceName, "-o", "yaml", "-n", namespace} - result, err := k8sTool.runKubectlCommand(ctx, args) - assert.NoError(t, err) - assert.NotNil(t, result) -} -func TestHandleKubectlGetTool(t *testing.T) { - t.Run("success with mocked kubectl", func(t *testing.T) { + t.Run("success", func(t *testing.T) { mock := utils.NewMockShellExecutor() - expectedOutput := `NAME READY STATUS RESTARTS AGE -pod1 1/1 Running 0 1d -pod2 1/1 Running 0 2d` - - mock.AddCommandString("kubectl", []string{"get", "pods", "-n", "default", "-o", "json"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + yamlContent := `apiVersion: v1 +kind: Pod +metadata: + name: test-pod +spec: + containers: + - name: test + image: nginx` - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + expectedOutput := `pod/test-pod created` + // Use partial matcher to handle dynamic temp file names + mock.AddPartialMatcherString("kubectl", []string{"create", "-f", "*"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + // We need to test the inline function from RegisterK8sTools + // Let's create a test handler that mimics the inline function + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + yamlContent := mcp.ParseString(request, "yaml_content", "") + + if yamlContent == "" { + return mcp.NewToolResultError("yaml_content is required"), nil + } + + // Create temporary file + tmpFile, err := os.CreateTemp("", "k8s-resource-*.yaml") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to create temp file: %v", err)), nil + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.WriteString(yamlContent); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to write to temp file: %v", err)), nil + } + tmpFile.Close() + + result, err := utils.RunCommandWithContext(ctx, "kubectl", []string{"create", "-f", tmpFile.Name()}) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Create command failed: %v", err)), nil + } + + return mcp.NewToolResultText(result), nil + } req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ - "resource_type": "pods", - "namespace": "default", - "output": "json", + "yaml_content": yamlContent, } - result, err := k8sTool.handleKubectlGetTool(ctx, req) + result, err := testHandler(ctx, req) assert.NoError(t, err) assert.NotNil(t, result) assert.False(t, result.IsError) // Verify the expected output content := getResultText(result) - assert.Contains(t, content, "pod1") - assert.Contains(t, content, "pod2") + assert.Contains(t, content, "created") - // Verify the correct kubectl command was called + // Verify kubectl create was called callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"get", "pods", "-n", "default", "-o", "json"}, callLog[0].Args) + assert.Len(t, callLog[0].Args, 3) // create, -f, + assert.Equal(t, "create", callLog[0].Args[0]) + assert.Equal(t, "-f", callLog[0].Args[1]) + // Third argument should be the temporary file path + assert.Contains(t, callLog[0].Args[2], "k8s-resource-") }) - t.Run("kubectl command failure", func(t *testing.T) { + t.Run("missing yaml_content parameter", func(t *testing.T) { mock := utils.NewMockShellExecutor() - mock.AddCommandString("kubectl", []string{"get", "invalidresource", "-o", "json"}, "", assert.AnError) ctx := utils.WithShellExecutor(context.Background(), mock) - clientset := fake.NewSimpleClientset() - k8sTool := newTestK8sTool(clientset) + // Test handler for missing parameter + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + yamlContent := mcp.ParseString(request, "yaml_content", "") + + if yamlContent == "" { + return mcp.NewToolResultError("yaml_content is required"), nil + } + return mcp.NewToolResultText("should not reach here"), nil + } req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ - "resource_type": "invalidresource", + // Missing yaml_content parameter } - result, err := k8sTool.handleKubectlGetTool(ctx, req) - assert.NoError(t, err) // MCP handlers should not return Go errors + result, err := testHandler(ctx, req) + assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "command kubectl failed") + assert.Contains(t, getResultText(result), "yaml_content is required") + + // Verify no commands were executed + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) }) } -func newMockLLM(response *llms.ContentResponse, err error) *mockLLM { - return &mockLLM{ - called: 0, - response: response, - error: err, - } -} +// Test the k8s_get_resource_yaml handler (inline function in RegisterK8sTools) +func TestHandleGetResourceYAML(t *testing.T) { + ctx := context.Background() -// not synchronized, don't use concurrently! -type mockLLM struct { - called int - response *llms.ContentResponse - error error -} + t.Run("success", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `apiVersion: v1 +kind: Pod +metadata: + name: test-pod + namespace: default +spec: + containers: + - name: test + image: nginx` + mock.AddCommandString("kubectl", []string{"get", "pod", "test-pod", "-o", "yaml", "-n", "default"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + // Test handler that mimics the inline function + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + resourceType := mcp.ParseString(request, "resource_type", "") + resourceName := mcp.ParseString(request, "resource_name", "") + namespace := mcp.ParseString(request, "namespace", "") + + if resourceType == "" || resourceName == "" { + return mcp.NewToolResultError("resource_type and resource_name are required"), nil + } + + args := []string{"get", resourceType, resourceName, "-o", "yaml"} + if namespace != "" { + args = append(args, "-n", namespace) + } + + result, err := utils.RunCommandWithContext(ctx, "kubectl", args) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Get YAML command failed: %v", err)), nil + } + + return mcp.NewToolResultText(result), nil + } -func (m *mockLLM) Call(ctx context.Context, prompt string, options ...llms.CallOption) (string, error) { - return llms.GenerateFromSinglePrompt(ctx, m, prompt, options...) -} + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "pod", + "resource_name": "test-pod", + "namespace": "default", + } -func (m *mockLLM) GenerateContent(ctx context.Context, _ []llms.MessageContent, options ...llms.CallOption) (*llms.ContentResponse, error) { - var opts llms.CallOptions - for _, opt := range options { - opt(&opts) - } + result, err := testHandler(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "test-pod") + assert.Contains(t, resultText, "apiVersion") + + // Verify the correct kubectl command was called + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Equal(t, "kubectl", callLog[0].Command) + assert.Equal(t, []string{"get", "pod", "test-pod", "-o", "yaml", "-n", "default"}, callLog[0].Args) + }) + + t.Run("missing parameters", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + ctx := utils.WithShellExecutor(context.Background(), mock) - if opts.StreamingFunc != nil && len(m.response.Choices) > 0 { - if err := opts.StreamingFunc(ctx, []byte(m.response.Choices[0].Content)); err != nil { - return nil, err + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + resourceType := mcp.ParseString(request, "resource_type", "") + resourceName := mcp.ParseString(request, "resource_name", "") + + if resourceType == "" || resourceName == "" { + return mcp.NewToolResultError("resource_type and resource_name are required"), nil + } + return mcp.NewToolResultText("should not reach here"), nil } - } - m.called++ + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "pod", + // Missing resource_name + } - return m.response, m.error + result, err := testHandler(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "resource_type and resource_name are required") + + // Verify no commands were executed + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) + }) + + t.Run("without namespace", func(t *testing.T) { + mock := utils.NewMockShellExecutor() + expectedOutput := `apiVersion: v1 +kind: ClusterRole +metadata: + name: test-cluster-role` + mock.AddCommandString("kubectl", []string{"get", "clusterrole", "test-cluster-role", "-o", "yaml"}, expectedOutput, nil) + ctx := utils.WithShellExecutor(ctx, mock) + + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + resourceType := mcp.ParseString(request, "resource_type", "") + resourceName := mcp.ParseString(request, "resource_name", "") + namespace := mcp.ParseString(request, "namespace", "") + + if resourceType == "" || resourceName == "" { + return mcp.NewToolResultError("resource_type and resource_name are required"), nil + } + + args := []string{"get", resourceType, resourceName, "-o", "yaml"} + if namespace != "" { + args = append(args, "-n", namespace) + } + + result, err := utils.RunCommandWithContext(ctx, "kubectl", args) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Get YAML command failed: %v", err)), nil + } + + return mcp.NewToolResultText(result), nil + } + + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "clusterrole", + "resource_name": "test-cluster-role", + // No namespace for cluster-scoped resource + } + + result, err := testHandler(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "test-cluster-role") + assert.Contains(t, resultText, "ClusterRole") + + // Verify the correct kubectl command was called (without namespace) + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Equal(t, "kubectl", callLog[0].Command) + assert.Equal(t, []string{"get", "clusterrole", "test-cluster-role", "-o", "yaml"}, callLog[0].Args) + }) } diff --git a/pkg/prometheus/prometheus.go b/pkg/prometheus/prometheus.go index 607fd134..dc4d6cb8 100644 --- a/pkg/prometheus/prometheus.go +++ b/pkg/prometheus/prometheus.go @@ -202,7 +202,7 @@ func handlePrometheusTargetsQueryTool(ctx context.Context, request mcp.CallToolR return mcp.NewToolResultText(string(prettyJSON)), nil } -func RegisterPrometheusTools(s *server.MCPServer) { +func RegisterPrometheusTools(s *server.MCPServer, kubeconfig string) { s.AddTool(mcp.NewTool("prometheus_query_tool", mcp.WithDescription("Execute a PromQL query against Prometheus"), mcp.WithString("query", mcp.Description("PromQL query to execute"), mcp.Required()), diff --git a/pkg/prometheus/prometheus_test.go b/pkg/prometheus/prometheus_test.go index 7b1b4c03..d51b52e5 100644 --- a/pkg/prometheus/prometheus_test.go +++ b/pkg/prometheus/prometheus_test.go @@ -1 +1,509 @@ package prometheus + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/stretchr/testify/assert" +) + +// mockRoundTripper is used to mock HTTP responses for testing +type mockRoundTripper struct { + response *http.Response + err error +} + +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if m.err != nil { + return nil, m.err + } + return m.response, nil +} + +func newTestClient(response *http.Response, err error) *http.Client { + return &http.Client{ + Transport: &mockRoundTripper{ + response: response, + err: err, + }, + } +} + +// Helper function to extract text content from MCP result +func getResultText(result *mcp.CallToolResult) string { + if result == nil || len(result.Content) == 0 { + return "" + } + if textContent, ok := result.Content[0].(mcp.TextContent); ok { + return textContent.Text + } + return "" +} + +// Helper function to create a mock HTTP response +func createMockResponse(statusCode int, body string) *http.Response { + return &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(strings.NewReader(body)), + Header: make(http.Header), + } +} + +// Helper function to create context with mock HTTP client +func contextWithMockClient(client *http.Client) context.Context { + return context.WithValue(context.Background(), clientKey{}, client) +} + +func TestHandlePrometheusQueryTool(t *testing.T) { + t.Run("successful query", func(t *testing.T) { + mockResponse := `{ + "status": "success", + "data": { + "resultType": "vector", + "result": [ + { + "metric": {"__name__": "up", "instance": "localhost:9090"}, + "value": [1609459200, "1"] + } + ] + } + }` + + client := newTestClient(createMockResponse(200, mockResponse), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up", + "prometheus_url": "http://localhost:9090", + } + + result, err := handlePrometheusQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + content := getResultText(result) + assert.Contains(t, content, "success") + assert.Contains(t, content, "up") + }) + + t.Run("missing query parameter", func(t *testing.T) { + ctx := context.Background() + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "prometheus_url": "http://localhost:9090", + } + + result, err := handlePrometheusQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "query parameter is required") + }) + + t.Run("HTTP error", func(t *testing.T) { + client := newTestClient(nil, assert.AnError) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up", + } + + result, err := handlePrometheusQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "failed to query Prometheus") + }) + + t.Run("HTTP 500 error", func(t *testing.T) { + client := newTestClient(createMockResponse(500, "Internal Server Error"), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up", + } + + result, err := handlePrometheusQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "Prometheus API error (500)") + }) + + t.Run("malformed JSON response", func(t *testing.T) { + client := newTestClient(createMockResponse(200, "invalid json {"), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up", + } + + result, err := handlePrometheusQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + // Should return raw response when JSON parsing fails + assert.Contains(t, getResultText(result), "invalid json") + }) + + t.Run("default prometheus URL", func(t *testing.T) { + mockResponse := `{"status": "success", "data": {"result": []}}` + client := newTestClient(createMockResponse(200, mockResponse), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up", + } + + result, err := handlePrometheusQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + }) +} + +func TestHandlePrometheusRangeQueryTool(t *testing.T) { + t.Run("successful range query", func(t *testing.T) { + mockResponse := `{ + "status": "success", + "data": { + "resultType": "matrix", + "result": [ + { + "metric": {"__name__": "up"}, + "values": [[1609459200, "1"], [1609459260, "1"]] + } + ] + } + }` + + client := newTestClient(createMockResponse(200, mockResponse), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up", + "start": "1609459200", + "end": "1609459260", + "step": "60s", + } + + result, err := handlePrometheusRangeQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + content := getResultText(result) + assert.Contains(t, content, "matrix") + assert.Contains(t, content, "values") + }) + + t.Run("missing query parameter", func(t *testing.T) { + ctx := context.Background() + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{} + + result, err := handlePrometheusRangeQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "query parameter is required") + }) + + t.Run("default time range and step", func(t *testing.T) { + mockResponse := `{"status": "success", "data": {"result": []}}` + client := newTestClient(createMockResponse(200, mockResponse), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up", + } + + result, err := handlePrometheusRangeQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + }) +} + +func TestHandlePrometheusLabelsQueryTool(t *testing.T) { + t.Run("successful labels query", func(t *testing.T) { + mockResponse := `{ + "status": "success", + "data": ["__name__", "instance", "job"] + }` + + client := newTestClient(createMockResponse(200, mockResponse), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{} + + result, err := handlePrometheusLabelsQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + content := getResultText(result) + assert.Contains(t, content, "__name__") + assert.Contains(t, content, "instance") + assert.Contains(t, content, "job") + }) + + t.Run("HTTP error", func(t *testing.T) { + client := newTestClient(nil, assert.AnError) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{} + + result, err := handlePrometheusLabelsQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "failed to query Prometheus") + }) + + t.Run("custom prometheus URL", func(t *testing.T) { + mockResponse := `{"status": "success", "data": []}` + client := newTestClient(createMockResponse(200, mockResponse), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "prometheus_url": "http://custom:9090", + } + + result, err := handlePrometheusLabelsQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + }) +} + +func TestHandlePrometheusTargetsQueryTool(t *testing.T) { + t.Run("successful targets query", func(t *testing.T) { + mockResponse := `{ + "status": "success", + "data": { + "activeTargets": [ + { + "discoveredLabels": {"__address__": "localhost:9090"}, + "labels": {"instance": "localhost:9090", "job": "prometheus"}, + "scrapePool": "prometheus", + "scrapeUrl": "http://localhost:9090/metrics", + "health": "up" + } + ] + } + }` + + client := newTestClient(createMockResponse(200, mockResponse), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{} + + result, err := handlePrometheusTargetsQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + content := getResultText(result) + assert.Contains(t, content, "activeTargets") + assert.Contains(t, content, "localhost:9090") + assert.Contains(t, content, "up") + }) + + t.Run("HTTP 404 error", func(t *testing.T) { + client := newTestClient(createMockResponse(404, "Not Found"), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{} + + result, err := handlePrometheusTargetsQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "Prometheus API error (404)") + }) +} + +func TestHandlePromql(t *testing.T) { + t.Run("missing query description", func(t *testing.T) { + ctx := context.Background() + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{} + + result, err := handlePromql(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "query_description is required") + }) + + t.Run("with query description", func(t *testing.T) { + ctx := context.Background() + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query_description": "CPU usage percentage", + } + + result, err := handlePromql(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + // This will likely fail due to missing OpenAI API key, but that's expected + // We're testing that the function handles the error gracefully + if result.IsError { + content := getResultText(result) + // Should contain an error about LLM client or API + assert.True(t, + strings.Contains(content, "failed to create LLM client") || + strings.Contains(content, "failed to generate content") || + strings.Contains(content, "API"), + ) + } + }) +} + +// Test context cancellation scenarios +func TestPrometheusToolsContextCancellation(t *testing.T) { + t.Run("query tool with cancelled context", func(t *testing.T) { + // Create a mock client that would block indefinitely + client := newTestClient(createMockResponse(200, `{"status": "success"}`), nil) + + // Create a cancelled context + cancelCtx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + ctx := contextWithMockClient(client) + _ = cancelCtx + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up", + } + + result, err := handlePrometheusQueryTool(ctx, request) + + // Should handle cancellation gracefully + assert.NoError(t, err) + assert.NotNil(t, result) + }) +} + +// Test edge cases and error scenarios +func TestPrometheusToolsEdgeCases(t *testing.T) { + t.Run("very large response", func(t *testing.T) { + // Create a large response (simulating large metrics data) + largeResponse := `{"status": "success", "data": {"result": [` + for i := 0; i < 1000; i++ { + if i > 0 { + largeResponse += "," + } + largeResponse += `{"metric": {"instance": "host` + string(rune(i)) + `"}, "value": [1609459200, "1"]}` + } + largeResponse += `]}}` + + client := newTestClient(createMockResponse(200, largeResponse), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up", + } + + result, err := handlePrometheusQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + content := getResultText(result) + assert.Contains(t, content, "success") + }) + + t.Run("special characters in query", func(t *testing.T) { + mockResponse := `{"status": "success", "data": {"result": []}}` + client := newTestClient(createMockResponse(200, mockResponse), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": `up{instance=~".*:9090"}`, + } + + result, err := handlePrometheusQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + }) + + t.Run("empty response body", func(t *testing.T) { + client := newTestClient(createMockResponse(200, ""), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up", + } + + result, err := handlePrometheusQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + }) +} + +// Test URL parameter encoding +func TestPrometheusURLEncoding(t *testing.T) { + t.Run("query with special characters", func(t *testing.T) { + mockResponse := `{"status": "success", "data": {"result": []}}` + client := newTestClient(createMockResponse(200, mockResponse), nil) + ctx := contextWithMockClient(client) + + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "query": "up{job=\"test service\"}", + } + + result, err := handlePrometheusQueryTool(ctx, request) + + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Test passes if no error occurs with special characters + content := getResultText(result) + assert.Contains(t, content, "success") + }) +} diff --git a/pkg/utils/datetime.go b/pkg/utils/datetime.go index f0c6d246..165ea683 100644 --- a/pkg/utils/datetime.go +++ b/pkg/utils/datetime.go @@ -2,15 +2,17 @@ package utils import ( "context" + "github.com/kagent-dev/tools/pkg/logger" "time" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) +var kubeConfig = "" + // DateTime tools using direct Go time package // This implementation matches the Python version exactly - func handleGetCurrentDateTimeTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Returns the current date and time in ISO 8601 format (RFC3339) // This matches the Python implementation: datetime.datetime.now().isoformat() @@ -18,7 +20,10 @@ func handleGetCurrentDateTimeTool(ctx context.Context, request mcp.CallToolReque return mcp.NewToolResultText(now.Format(time.RFC3339)), nil } -func RegisterDateTimeTools(s *server.MCPServer) { +func RegisterDateTimeTools(s *server.MCPServer, kubeconfig string) { + kubeConfig = kubeconfig + logger.Get().Info("kubeConfig", kubeConfig) + // Register the GetCurrentDateTime tool to match Python implementation exactly s.AddTool(mcp.NewTool("datetime_get_current_time", mcp.WithDescription("Returns the current date and time in ISO 8601 format."), From a5fde94259269aa6c6e07f28ac44a4cf1660c5ed Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Tue, 8 Jul 2025 08:11:40 -0400 Subject: [PATCH 16/41] stremable-http (#4) Signed-off-by: Eitan Yarmush --- cmd/main.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 67222dff..5c44309f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,10 +4,6 @@ import ( "context" "errors" "fmt" - "github.com/joho/godotenv" - "github.com/kagent-dev/tools/internal/version" - "github.com/kagent-dev/tools/pkg/logger" - "github.com/kagent-dev/tools/pkg/utils" "net/http" "os" "os/signal" @@ -16,6 +12,11 @@ import ( "syscall" "time" + "github.com/joho/godotenv" + "github.com/kagent-dev/tools/internal/version" + "github.com/kagent-dev/tools/pkg/logger" + "github.com/kagent-dev/tools/pkg/utils" + "github.com/kagent-dev/tools/pkg/argo" "github.com/kagent-dev/tools/pkg/cilium" "github.com/kagent-dev/tools/pkg/helm" @@ -90,7 +91,7 @@ func run(cmd *cobra.Command, args []string) { signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) // HTTP server reference (only used when not in stdio mode) - var sseServer *server.SSEServer + var sseServer *server.StreamableHTTPServer // Start server based on chosen mode wg.Add(1) @@ -100,7 +101,7 @@ func run(cmd *cobra.Command, args []string) { runStdioServer(ctx, mcp) }() } else { - sseServer = server.NewSSEServer(mcp) + sseServer = server.NewStreamableHTTPServer(mcp) go func() { defer wg.Done() addr := fmt.Sprintf(":%d", port) From d354a4b478b909534f2ae7c3eb09df318a26823b Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Tue, 15 Jul 2025 09:01:14 +0200 Subject: [PATCH 17/41] Feature/tools OTEL (#7) - added telemetry - security validations - structured logging - e2e tests Signed-off-by: Dmytro Rashko --- .devcontainer/devcontainer.json | 3 + .github/workflows/ci.yaml | 19 +- .gitignore | 6 +- Makefile | 26 +- cmd/main.go | 189 +++-- e2e/e2e_test.go | 1005 +++++++++++++++++++++++++ go.mod | 48 +- go.sum | 141 +--- internal/cache/cache.go | 545 ++++++++++++++ internal/cache/cache_test.go | 488 ++++++++++++ internal/cmd/cmd.go | 69 ++ internal/cmd/cmd_test.go | 58 ++ internal/cmd/mock.go | 120 +++ internal/commands/builder.go | 747 ++++++++++++++++++ internal/commands/builder_test.go | 585 ++++++++++++++ internal/errors/tool_errors.go | 352 +++++++++ internal/errors/tool_errors_test.go | 366 +++++++++ internal/logger/logger.go | 76 ++ internal/logger/logger_test.go | 72 ++ internal/security/validation.go | 291 +++++++ internal/security/validation_test.go | 322 ++++++++ internal/telemetry/config.go | 74 ++ internal/telemetry/config_test.go | 49 ++ internal/telemetry/middleware.go | 179 +++++ internal/telemetry/middleware_test.go | 801 ++++++++++++++++++++ internal/telemetry/tracing.go | 282 +++++++ internal/telemetry/tracing_test.go | 374 +++++++++ pkg/argo/argo.go | 49 +- pkg/argo/argo_test.go | 126 ++-- pkg/cilium/cilium.go | 651 ++++++++-------- pkg/cilium/cilium_test.go | 304 ++++++-- pkg/helm/helm.go | 89 ++- pkg/helm/helm_test.go | 176 +++-- pkg/istio/istio.go | 108 ++- pkg/istio/istio_test.go | 838 ++++----------------- pkg/k8s/k8s.go | 224 ++++-- pkg/k8s/k8s_test.go | 434 +++-------- pkg/logger/logger.go | 58 -- pkg/logger/logger_test.go | 83 -- pkg/prometheus/prometheus.go | 133 +++- pkg/prometheus/prometheus_test.go | 6 +- pkg/utils/common.go | 337 ++------- pkg/utils/common_test.go | 288 ------- pkg/utils/datetime.go | 32 +- 44 files changed, 8530 insertions(+), 2693 deletions(-) create mode 100644 e2e/e2e_test.go create mode 100644 internal/cache/cache.go create mode 100644 internal/cache/cache_test.go create mode 100644 internal/cmd/cmd.go create mode 100644 internal/cmd/cmd_test.go create mode 100644 internal/cmd/mock.go create mode 100644 internal/commands/builder.go create mode 100644 internal/commands/builder_test.go create mode 100644 internal/errors/tool_errors.go create mode 100644 internal/errors/tool_errors_test.go create mode 100644 internal/logger/logger.go create mode 100644 internal/logger/logger_test.go create mode 100644 internal/security/validation.go create mode 100644 internal/security/validation_test.go create mode 100644 internal/telemetry/config.go create mode 100644 internal/telemetry/config_test.go create mode 100644 internal/telemetry/middleware.go create mode 100644 internal/telemetry/middleware_test.go create mode 100644 internal/telemetry/tracing.go create mode 100644 internal/telemetry/tracing_test.go delete mode 100644 pkg/logger/logger.go delete mode 100644 pkg/logger/logger_test.go delete mode 100644 pkg/utils/common_test.go diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9b7a19c8..22b0a48f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -48,6 +48,9 @@ //forward the following ports "forwardPorts": [8084], + //network + "network": "host", + //mount docker directly on the host "mounts": ["source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"], diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a857e6e2..4d1c4d63 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,4 +53,21 @@ jobs: - name: Run cmd/main.go tests working-directory: . run: | - go test -v ./... + make test + + go-e2e-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.24" + cache: true + + - name: Run cmd/main.go tests + working-directory: . + run: | + make e2e diff --git a/.gitignore b/.gitignore index d146fbfc..2496f99b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ bin/ .env.local .env.development.local .env.test.local -.env.production.local \ No newline at end of file +.env.production.local +/logs/ +/kagent-tools +/*.out +*.html diff --git a/Makefile b/Makefile index 90924db4..1d9fdd11 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,10 @@ LDFLAGS := -X github.com/kagent-dev/tools/internal/version.Version=$(VERSION) -X ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin +.PHONY: clean +clean: + rm -rf ./bin/kagent-tools-* + .PHONY: fmt fmt: ## Run go fmt against code. go fmt ./... @@ -23,11 +27,11 @@ vet: ## Run go vet against code. .PHONY: lint lint: golangci-lint ## Run golangci-lint linter - $(GOLANGCI_LINT) run + $(GOLANGCI_LINT) run --build-tags=test .PHONY: lint-fix lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes - $(GOLANGCI_LINT) run --fix + $(GOLANGCI_LINT) run --build-tags=test --fix .PHONY: lint-config lint-config: golangci-lint ## Verify golangci-lint linter configuration @@ -43,8 +47,16 @@ tidy: ## Run go mod tidy to ensure dependencies are up to date. go mod tidy .PHONY: test -test: - go test -v -cover ./... +test: build lint ## Run all tests with build, lint, and coverage + go test -tags=test -v -cover ./pkg/... ./internal/... + +.PHONY: test-only +test-only: ## Run tests only (without build/lint for faster iteration) + go test -tags=test -v -cover ./pkg/... ./internal/... + +.PHONY: e2e +e2e: test docker-build + go test -tags=test -v -cover ./e2e/... bin/kagent-tools-linux-amd64: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/kagent-tools-linux-amd64 ./cmd @@ -143,6 +155,12 @@ docker-build-all: DOCKER_BUILD_ARGS = --progress=plain --builder $(BUILDX_BUILDE docker-build-all: $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f Dockerfile ./ +.PHONY: kind-update-kagent +kind-update-kagent: docker-build + kind get clusters | grep -q $(KIND_CLUSTER_NAME) || kind create cluster --name $(KIND_CLUSTER_NAME) + kind load docker-image --name $(KIND_CLUSTER_NAME) $(TOOLS_IMG) + kubectl patch --namespace kagent deployment/kagent --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/3/image", "value": "$(TOOLS_IMG)"}]' + ## Tool Binaries ## Location to install dependencies t diff --git a/cmd/main.go b/cmd/main.go index 5c44309f..6ea40ae7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,24 +7,29 @@ import ( "net/http" "os" "os/signal" + "runtime" "strings" "sync" "syscall" "time" "github.com/joho/godotenv" + "github.com/kagent-dev/tools/internal/logger" + "github.com/kagent-dev/tools/internal/telemetry" "github.com/kagent-dev/tools/internal/version" - "github.com/kagent-dev/tools/pkg/logger" - "github.com/kagent-dev/tools/pkg/utils" - "github.com/kagent-dev/tools/pkg/argo" "github.com/kagent-dev/tools/pkg/cilium" "github.com/kagent-dev/tools/pkg/helm" "github.com/kagent-dev/tools/pkg/istio" "github.com/kagent-dev/tools/pkg/k8s" "github.com/kagent-dev/tools/pkg/prometheus" - "github.com/mark3labs/mcp-go/server" + "github.com/kagent-dev/tools/pkg/utils" "github.com/spf13/cobra" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + + "github.com/mark3labs/mcp-go/server" ) var ( @@ -69,12 +74,36 @@ func run(cmd *cobra.Command, args []string) { logger.Init() defer logger.Sync() - logger.Get().Info("Starting "+Name, "version", Version, "git_commit", GitCommit, "build_date", BuildDate) - // Setup context with cancellation for graceful shutdown ctx, cancel := context.WithCancel(context.Background()) defer cancel() + // Initialize OpenTelemetry tracing + cfg := telemetry.LoadOtelCfg() + + err := telemetry.SetupOTelSDK(ctx) + if err != nil { + logger.Get().Error("Failed to setup OpenTelemetry SDK", "error", err) + os.Exit(1) + } + + // Start root span for server lifecycle + tracer := otel.Tracer("kagent-tools/server") + ctx, rootSpan := tracer.Start(ctx, "server.lifecycle") + defer rootSpan.End() + + rootSpan.SetAttributes( + attribute.String("server.name", Name), + attribute.String("server.version", cfg.Telemetry.ServiceVersion), + attribute.String("server.git_commit", GitCommit), + attribute.String("server.build_date", BuildDate), + attribute.Bool("server.stdio_mode", stdio), + attribute.Int("server.port", port), + attribute.StringSlice("server.tools", tools), + ) + + logger.Get().Info("Starting "+Name, "version", Version, "git_commit", GitCommit, "build_date", BuildDate) + mcp := server.NewMCPServer( Name, Version, @@ -91,7 +120,7 @@ func run(cmd *cobra.Command, args []string) { signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) // HTTP server reference (only used when not in stdio mode) - var sseServer *server.StreamableHTTPServer + var httpServer *http.Server // Start server based on chosen mode wg.Add(1) @@ -101,16 +130,49 @@ func run(cmd *cobra.Command, args []string) { runStdioServer(ctx, mcp) }() } else { - sseServer = server.NewStreamableHTTPServer(mcp) + sseServer := server.NewStreamableHTTPServer(mcp) + + // Create a mux to handle different routes + mux := http.NewServeMux() + + // Add health endpoint + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + if err := writeResponse(w, []byte("OK")); err != nil { + logger.Get().Error("Failed to write health response", "error", err) + } + }) + + // Add metrics endpoint (basic implementation for e2e tests) + mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + + // Generate real runtime metrics instead of hardcoded values + metrics := generateRuntimeMetrics() + if err := writeResponse(w, []byte(metrics)); err != nil { + logger.Get().Error("Failed to write metrics response", "error", err) + } + }) + + // Handle all other routes with the MCP server wrapped in telemetry middleware + mux.Handle("/", telemetry.HTTPMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sseServer.ServeHTTP(w, r) + }))) + + httpServer = &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: mux, + } + go func() { defer wg.Done() - addr := fmt.Sprintf(":%d", port) - logger.Get().Info("Running KAgent Tools Server", "port", addr, "tools", strings.Join(tools, ",")) - if err := sseServer.Start(addr); err != nil { + logger.Get().Info("Running KAgent Tools Server", "port", fmt.Sprintf(":%d", port), "tools", strings.Join(tools, ",")) + if err := httpServer.ListenAndServe(); err != nil { if !errors.Is(err, http.ErrServerClosed) { - logger.Get().Error(err, "Failed to start SSE server") + logger.Get().Error("Failed to start HTTP server", "error", err) } else { - logger.Get().Info("SSE server closed gracefully.") + logger.Get().Info("HTTP server closed gracefully.") } } }() @@ -121,16 +183,23 @@ func run(cmd *cobra.Command, args []string) { <-signalChan logger.Get().Info("Received termination signal, shutting down server...") + // Mark root span as shutting down + rootSpan.AddEvent("server.shutdown.initiated") + // Cancel context to notify any context-aware operations cancel() // Gracefully shutdown HTTP server if running - if !stdio && sseServer != nil { + if !stdio && httpServer != nil { shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) defer shutdownCancel() - if err := sseServer.Shutdown(shutdownCtx); err != nil { - logger.Get().Error(err, "Failed to shutdown server gracefully") + if err := httpServer.Shutdown(shutdownCtx); err != nil { + logger.Get().Error("Failed to shutdown server gracefully", "error", err) + rootSpan.RecordError(err) + rootSpan.SetStatus(codes.Error, "Server shutdown failed") + } else { + rootSpan.AddEvent("server.shutdown.completed") } } }() @@ -140,6 +209,53 @@ func run(cmd *cobra.Command, args []string) { logger.Get().Info("Server shutdown complete") } +// writeResponse writes data to an HTTP response writer with proper error handling +func writeResponse(w http.ResponseWriter, data []byte) error { + _, err := w.Write(data) + return err +} + +// generateRuntimeMetrics generates real runtime metrics for the /metrics endpoint +func generateRuntimeMetrics() string { + var m runtime.MemStats + runtime.ReadMemStats(&m) + + now := time.Now().Unix() + + // Build metrics in Prometheus format + metrics := strings.Builder{} + + // Go runtime info + metrics.WriteString("# HELP go_info Information about the Go environment.\n") + metrics.WriteString("# TYPE go_info gauge\n") + metrics.WriteString(fmt.Sprintf("go_info{version=\"%s\"} 1\n", runtime.Version())) + + // Process start time + metrics.WriteString("# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.\n") + metrics.WriteString("# TYPE process_start_time_seconds gauge\n") + metrics.WriteString(fmt.Sprintf("process_start_time_seconds %d\n", now)) + + // Memory metrics + metrics.WriteString("# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.\n") + metrics.WriteString("# TYPE go_memstats_alloc_bytes gauge\n") + metrics.WriteString(fmt.Sprintf("go_memstats_alloc_bytes %d\n", m.Alloc)) + + metrics.WriteString("# HELP go_memstats_total_alloc_bytes Total number of bytes allocated, even if freed.\n") + metrics.WriteString("# TYPE go_memstats_total_alloc_bytes counter\n") + metrics.WriteString(fmt.Sprintf("go_memstats_total_alloc_bytes %d\n", m.TotalAlloc)) + + metrics.WriteString("# HELP go_memstats_sys_bytes Number of bytes obtained from system.\n") + metrics.WriteString("# TYPE go_memstats_sys_bytes gauge\n") + metrics.WriteString(fmt.Sprintf("go_memstats_sys_bytes %d\n", m.Sys)) + + // Goroutine count + metrics.WriteString("# HELP go_goroutines Number of goroutines that currently exist.\n") + metrics.WriteString("# TYPE go_goroutines gauge\n") + metrics.WriteString(fmt.Sprintf("go_goroutines %d\n", runtime.NumGoroutine())) + + return metrics.String() +} + func runStdioServer(ctx context.Context, mcp *server.MCPServer) { logger.Get().Info("Running KAgent Tools Server STDIO:", "tools", strings.Join(tools, ",")) stdioServer := server.NewStdioServer(mcp) @@ -149,39 +265,28 @@ func runStdioServer(ctx context.Context, mcp *server.MCPServer) { } func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string) { - - var toolProviderMap = map[string]func(*server.MCPServer, string){ - "utils": utils.RegisterDateTimeTools, - "k8s": k8s.RegisterK8sTools, - "prometheus": prometheus.RegisterPrometheusTools, - "helm": helm.RegisterHelmTools, - "istio": istio.RegisterIstioTools, - "argo": argo.RegisterArgoTools, - "cilium": cilium.RegisterCiliumTools, + // A map to hold tool providers and their registration functions + toolProviderMap := map[string]func(*server.MCPServer){ + "argo": argo.RegisterTools, + "cilium": cilium.RegisterTools, + "helm": helm.RegisterTools, + "istio": istio.RegisterTools, + "k8s": func(s *server.MCPServer) { k8s.RegisterTools(s, nil, kubeconfig) }, + "prometheus": prometheus.RegisterTools, + "utils": utils.RegisterTools, } - if len(kubeconfig) > 0 { - logger.Get().Info("Using kubeconfig file", "path", kubeconfig) - } - - // If no tools specified, register all tools + // If no specific tools are specified, register all available tools. if len(enabledToolProviders) == 0 { - logger.Get().Info("No specific tools provided, registering all tools") - for toolProvider, registerFunc := range toolProviderMap { - logger.Get().Info("Registering tools", "provider", toolProvider) - registerFunc(mcp, kubeconfig) + for name := range toolProviderMap { + enabledToolProviders = append(enabledToolProviders, name) } - return } - - // Register only the specified tools - logger.Get().Info("provider list", "tools", enabledToolProviders) for _, toolProviderName := range enabledToolProviders { - if registerFunc, ok := toolProviderMap[strings.ToLower(toolProviderName)]; ok { - logger.Get().Info("Registering tool", "provider", toolProviderName) - registerFunc(mcp, kubeconfig) + if registerFunc, ok := toolProviderMap[toolProviderName]; ok { + registerFunc(mcp) } else { - logger.Get().Error(nil, "Unknown tool specified", "provider", toolProviderName) + logger.Get().Error("Unknown tool specified", "provider", toolProviderName) } } } diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go new file mode 100644 index 00000000..ec047514 --- /dev/null +++ b/e2e/e2e_test.go @@ -0,0 +1,1005 @@ +package e2e + +import ( + "bufio" + "context" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// getBinaryName returns the platform-specific binary name +func getBinaryName() string { + osName := runtime.GOOS + archName := runtime.GOARCH + return fmt.Sprintf("kagent-tools-%s-%s", osName, archName) +} + +// TestServerConfig holds configuration for server tests +type TestServerConfig struct { + Port int + Tools []string + Kubeconfig string + Stdio bool + Timeout time.Duration +} + +// ServerTestResult holds the result of a server test +type ServerTestResult struct { + Output string + Error error + Duration time.Duration +} + +// TestServer represents a test server instance +type TestServer struct { + cmd *exec.Cmd + port int + stdio bool + cancel context.CancelFunc + done chan struct{} + output strings.Builder + mu sync.RWMutex +} + +// NewTestServer creates a new test server instance +func NewTestServer(config TestServerConfig) *TestServer { + return &TestServer{ + port: config.Port, + stdio: config.Stdio, + done: make(chan struct{}), + } +} + +// Start starts the test server +func (ts *TestServer) Start(ctx context.Context, config TestServerConfig) error { + ts.mu.Lock() + defer ts.mu.Unlock() + + // Build command arguments + args := []string{} + if config.Stdio { + args = append(args, "--stdio") + } else { + args = append(args, "--port", fmt.Sprintf("%d", config.Port)) + } + + if len(config.Tools) > 0 { + args = append(args, "--tools", strings.Join(config.Tools, ",")) + } + + if config.Kubeconfig != "" { + args = append(args, "--kubeconfig", config.Kubeconfig) + } + + // Create context with cancellation + ctx, cancel := context.WithCancel(ctx) + ts.cancel = cancel + + // Start server process + binaryName := getBinaryName() + ts.cmd = exec.CommandContext(ctx, fmt.Sprintf("../bin/%s", binaryName), args...) + ts.cmd.Env = append(os.Environ(), "LOG_LEVEL=debug") + + // Set up output capture + stdout, err := ts.cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("failed to create stdout pipe: %w", err) + } + + stderr, err := ts.cmd.StderrPipe() + if err != nil { + return fmt.Errorf("failed to create stderr pipe: %w", err) + } + + // Start the command + if err := ts.cmd.Start(); err != nil { + return fmt.Errorf("failed to start server: %w", err) + } + + // Start goroutines to capture output + go ts.captureOutput(stdout, "STDOUT") + go ts.captureOutput(stderr, "STDERR") + + // Wait for server to start + if !config.Stdio { + return ts.waitForHTTPServer(ctx, config.Timeout) + } + + return nil +} + +// Stop stops the test server +func (ts *TestServer) Stop() error { + ts.mu.Lock() + defer ts.mu.Unlock() + + if ts.cancel != nil { + ts.cancel() + } + + if ts.cmd != nil && ts.cmd.Process != nil { + // Send interrupt signal for graceful shutdown + if err := ts.cmd.Process.Signal(os.Interrupt); err != nil { + // If interrupt fails, kill the process + _ = ts.cmd.Process.Kill() + } + + // Wait for process to exit with timeout + done := make(chan error, 1) + go func() { + done <- ts.cmd.Wait() + }() + + select { + case <-done: + // Process exited + case <-time.After(5 * time.Second): + // Timeout, force kill + _ = ts.cmd.Process.Kill() + } + } + + close(ts.done) + return nil +} + +// GetOutput returns the captured output +func (ts *TestServer) GetOutput() string { + ts.mu.RLock() + defer ts.mu.RUnlock() + return ts.output.String() +} + +// captureOutput captures output from the server +func (ts *TestServer) captureOutput(reader io.Reader, prefix string) { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := scanner.Text() + ts.mu.Lock() + ts.output.WriteString(fmt.Sprintf("[%s] %s\n", prefix, line)) + ts.mu.Unlock() + } +} + +// waitForHTTPServer waits for the HTTP server to become available +func (ts *TestServer) waitForHTTPServer(ctx context.Context, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + url := fmt.Sprintf("http://localhost:%d/health", ts.port) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for server to start") + case <-ticker.C: + resp, err := http.Get(url) + if err == nil { + resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return nil + } + } + } + } +} + +// TestHTTPServerStartup tests basic HTTP server startup and shutdown +func TestHTTPServerStartup(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8085, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait a bit for server to be fully ready + time.Sleep(3 * time.Second) + + // Test health endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + require.NoError(t, err, "Health endpoint should be accessible") + assert.Equal(t, http.StatusOK, resp.StatusCode) + resp.Body.Close() + + // Check server output + output := server.GetOutput() + assert.Contains(t, output, "Running KAgent Tools Server") + assert.Contains(t, output, fmt.Sprintf(":%d", config.Port)) + + // Stop server + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") + + // Verify server is stopped + time.Sleep(1 * time.Second) + _, err = http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + assert.Error(t, err, "Server should not be accessible after stop") +} + +// TestHTTPServerWithSpecificTools tests server with specific tools enabled +func TestHTTPServerWithSpecificTools(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8086, + Tools: []string{"utils", "k8s"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for tool registration + output := server.GetOutput() + assert.Contains(t, output, "RegisterTools initialized", "Should register specified tools") + assert.Contains(t, output, "utils", "Should register utils tools") + assert.Contains(t, output, "k8s", "Should register k8s tools") + + // Stop server + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestHTTPServerWithAllTools tests server with all tools enabled (default) +func TestHTTPServerWithAllTools(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8087, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for all tools registration + output := server.GetOutput() + assert.Contains(t, output, "RegisterTools initialized", "Should initialize RegisterTools") + + // Verify server is running (tools are implicitly registered when no specific tools are provided) + assert.Contains(t, output, "Running KAgent Tools Server", "Should be running with all tools") + + // Stop server + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestHTTPServerWithKubeconfig tests server with kubeconfig parameter +func TestHTTPServerWithKubeconfig(t *testing.T) { + ctx := context.Background() + + // Create a temporary kubeconfig file + tempDir := t.TempDir() + kubeconfigPath := filepath.Join(tempDir, "kubeconfig") + + kubeconfigContent := `apiVersion: v1 +kind: Config +clusters: +- cluster: + server: https://test-cluster + name: test-cluster +contexts: +- context: + cluster: test-cluster + user: test-user + name: test-context +current-context: test-context +users: +- name: test-user + user: + token: test-token +` + + err := os.WriteFile(kubeconfigPath, []byte(kubeconfigContent), 0644) + require.NoError(t, err, "Should create temporary kubeconfig file") + + config := TestServerConfig{ + Port: 8088, + Kubeconfig: kubeconfigPath, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err = server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for kubeconfig setting + output := server.GetOutput() + assert.Contains(t, output, "RegisterTools initialized", "Should initialize RegisterTools") + assert.Contains(t, output, "Running KAgent Tools Server", "Should be running with kubeconfig") + + // Stop server + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestStdioServer tests STDIO server mode +func TestStdioServer(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Stdio: true, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for STDIO mode + output := server.GetOutput() + assert.Contains(t, output, "Running KAgent Tools Server STDIO") + + // Stop server + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestServerGracefulShutdown tests graceful shutdown behavior +func TestServerGracefulShutdown(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8100, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Stop server and measure shutdown time + start := time.Now() + err = server.Stop() + duration := time.Since(start) + + require.NoError(t, err, "Server should stop gracefully") + assert.Less(t, duration, 10*time.Second, "Shutdown should complete within reasonable time") + + // Wait a bit for shutdown logs to be captured + time.Sleep(3 * time.Second) + + // Check server output for graceful shutdown + output := server.GetOutput() + // The main test is that the server started successfully and stopped without error + assert.Contains(t, output, "Running KAgent Tools Server", "Server should have started successfully") + + // Try to verify the server is actually stopped by attempting to connect + _, err = http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + assert.Error(t, err, "Server should not be accessible after stop") +} + +// TestServerWithInvalidTool tests server behavior with invalid tool names +func TestServerWithInvalidTool(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8090, + Tools: []string{"invalid-tool", "utils"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start even with invalid tools") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for error about invalid tool + output := server.GetOutput() + assert.Contains(t, output, "Unknown tool specified") + assert.Contains(t, output, "invalid-tool") + + // Valid tools should still be registered + assert.Contains(t, output, "RegisterTools initialized") + assert.Contains(t, output, "utils") + + // Stop server + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestServerVersionAndBuildInfo tests server version and build information +func TestServerVersionAndBuildInfo(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8091, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for version information + output := server.GetOutput() + assert.Contains(t, output, "Starting kagent-tools-server") + assert.Contains(t, output, "version") + + // Stop server + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestConcurrentServerInstances tests running multiple server instances +func TestConcurrentServerInstances(t *testing.T) { + ctx := context.Background() + + var wg sync.WaitGroup + numServers := 3 + servers := make([]*TestServer, numServers) + + // Start multiple servers on different ports + for i := 0; i < numServers; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + + config := TestServerConfig{ + Port: 8092 + index, + Tools: []string{"utils"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + servers[index] = server + + err := server.Start(ctx, config) + assert.NoError(t, err, fmt.Sprintf("Server %d should start successfully", index)) + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Test health endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + assert.NoError(t, err, fmt.Sprintf("Health endpoint should be accessible for server %d", index)) + if resp != nil { + resp.Body.Close() + } + }(i) + } + + wg.Wait() + + // Stop all servers + for i, server := range servers { + if server != nil { + err := server.Stop() + assert.NoError(t, err, fmt.Sprintf("Server %d should stop gracefully", i)) + } + } +} + +// TestServerEnvironmentVariables tests server with environment variables +func TestServerEnvironmentVariables(t *testing.T) { + ctx := context.Background() + + // Set environment variables + originalEnv := os.Environ() + defer func() { + os.Clearenv() + for _, env := range originalEnv { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + os.Setenv(parts[0], parts[1]) + } + } + }() + + os.Setenv("LOG_LEVEL", "info") + os.Setenv("OTEL_SERVICE_NAME", "test-kagent-tools") + + config := TestServerConfig{ + Port: 8095, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output + output := server.GetOutput() + assert.Contains(t, output, "Starting kagent-tools-server") + + // Stop server + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestServerBuildAndExecution tests that the server binary exists and is executable +func TestServerBuildAndExecution(t *testing.T) { + // Check if server binary exists + binaryName := getBinaryName() + binaryPath := fmt.Sprintf("../bin/%s", binaryName) + _, err := os.Stat(binaryPath) + if os.IsNotExist(err) { + t.Skip("Server binary not found, skipping test. Run 'make build' first.") + } + require.NoError(t, err, "Server binary should exist") + + // Test --help flag + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, binaryPath, "--help") + output, err := cmd.CombinedOutput() + require.NoError(t, err, "Server should respond to --help flag") + + outputStr := string(output) + assert.Contains(t, outputStr, "KAgent tool server") + assert.Contains(t, outputStr, "--port") + assert.Contains(t, outputStr, "--stdio") + assert.Contains(t, outputStr, "--tools") + assert.Contains(t, outputStr, "--kubeconfig") +} + +// Benchmark tests +func BenchmarkServerStartup(b *testing.B) { + ctx := context.Background() + + for i := 0; i < b.N; i++ { + config := TestServerConfig{ + Port: 8096 + i, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + start := time.Now() + err := server.Start(ctx, config) + if err != nil { + b.Fatalf("Server startup failed: %v", err) + } + + // Wait for server to be ready + time.Sleep(1 * time.Second) + + duration := time.Since(start) + b.ReportMetric(float64(duration.Nanoseconds()), "startup_time_ns") + + // Stop server + _ = server.Stop() + } +} + +// Helper functions for test setup +func init() { + // Ensure the binary exists before running tests + binaryName := getBinaryName() + binaryPath := fmt.Sprintf("../bin/%s", binaryName) + if _, err := os.Stat(binaryPath); os.IsNotExist(err) { + // Try to build the binary + cmd := exec.Command("make", "build") + cmd.Dir = ".." + if err := cmd.Run(); err != nil { + panic(fmt.Sprintf("Failed to build server binary: %v", err)) + } + } +} + +// TestToolRegistrationValidation tests that tool registration works correctly +func TestToolRegistrationValidation(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + config TestServerConfig + expectedTools []string + shouldFail bool + }{ + { + name: "Register single tool", + config: TestServerConfig{ + Port: 8087, + Tools: []string{"k8s"}, + Timeout: 30 * time.Second, + }, + expectedTools: []string{"k8s"}, + shouldFail: false, + }, + { + name: "Register multiple tools", + config: TestServerConfig{ + Port: 8088, + Tools: []string{"k8s", "prometheus", "utils"}, + Timeout: 30 * time.Second, + }, + expectedTools: []string{"k8s", "prometheus", "utils"}, + shouldFail: false, + }, + { + name: "Register invalid tool", + config: TestServerConfig{ + Port: 8089, + Tools: []string{"invalid-tool"}, + Timeout: 30 * time.Second, + }, + shouldFail: false, + }, + { + name: "Register all tools implicitly", + config: TestServerConfig{ + Port: 8090, + Tools: []string{}, + Timeout: 30 * time.Second, + }, + expectedTools: []string{"utils", "k8s", "prometheus", "helm", "istio", "argo", "cilium"}, + shouldFail: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + server := NewTestServer(tc.config) + err := server.Start(ctx, tc.config) + + if tc.shouldFail { + require.Error(t, err, "Server should fail to start with invalid configuration") + return + } + + require.NoError(t, err, "Server should start successfully") + defer func() { + if err := server.Stop(); err != nil { + t.Errorf("Failed to stop server: %v", err) + } + }() + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Verify registered tools + output := server.GetOutput() + + // Special handling for invalid tool test case + if tc.name == "Register invalid tool" { + assert.Contains(t, output, "Unknown tool specified", "Should warn about invalid tool") + assert.Contains(t, output, "invalid-tool", "Should mention the invalid tool name") + } else { + if tc.name == "Register all tools implicitly" { + // For implicit all tools registration, check for RegisterTools initialized + assert.Contains(t, output, "RegisterTools initialized", "Should initialize RegisterTools") + // Don't check for individual tool names as they're not logged individually + assert.Contains(t, output, "Running KAgent Tools Server", "Should be running with all tools") + } else { + // For specific tools, check for Running server message and tool names + assert.Contains(t, output, "Running KAgent Tools Server", "Should be running server") + for _, tool := range tc.expectedTools { + assert.Contains(t, output, tool, fmt.Sprintf("Should register %s tool", tool)) + } + } + } + + // Test health endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", tc.config.Port)) + require.NoError(t, err, "Health endpoint should be accessible") + assert.Equal(t, http.StatusOK, resp.StatusCode) + resp.Body.Close() + }) + } +} + +// TestToolExecutionFlow tests the complete flow of tool execution +func TestToolExecutionFlow(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8091, + Tools: []string{"utils"}, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + defer func() { + if err := server.Stop(); err != nil { + t.Errorf("Failed to stop server: %v", err) + } + }() + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Test health endpoint (MCP server doesn't have REST endpoints for tool execution) + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + require.NoError(t, err, "Should execute request successfully") + defer resp.Body.Close() + + // Check response + assert.Equal(t, http.StatusOK, resp.StatusCode, "Should return OK status") + + // Read response body + body, err := io.ReadAll(resp.Body) + require.NoError(t, err, "Should read response body") + + // Response should contain "OK" + assert.Equal(t, "OK", string(body), "Should return OK response") +} + +// TestServerTelemetry tests that telemetry is properly initialized and working +func TestServerTelemetry(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8092, + Tools: []string{"utils"}, + Timeout: 30 * time.Second, + } + + // Set test environment variables for telemetry + os.Setenv("OTEL_SERVICE_NAME", "kagent-tools-test") + os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317") + defer os.Unsetenv("OTEL_SERVICE_NAME") + defer os.Unsetenv("OTEL_EXPORTER_OTLP_ENDPOINT") + + server := NewTestServer(config) + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + defer func() { + if err := server.Stop(); err != nil { + t.Errorf("Failed to stop server: %v", err) + } + }() + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for telemetry initialization + output := server.GetOutput() + assert.Contains(t, output, "Starting kagent-tools-server", "Server should start with telemetry") + + // Make a request to generate telemetry + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + require.NoError(t, err, "Health endpoint should be accessible") + assert.Equal(t, http.StatusOK, resp.StatusCode) + resp.Body.Close() + + // Check server output for successful startup (telemetry is initialized internally) + output = server.GetOutput() + assert.Contains(t, output, "Running KAgent Tools Server", "Server should be running with telemetry enabled") +} + +// TestToolRegistrationWithInvalidNames tests server behavior with invalid tool names +func TestToolRegistrationWithInvalidNames(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8087, + Tools: []string{"invalid-tool", "not-exists", "k8s"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully despite invalid tools") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for warning messages about invalid tools + output := server.GetOutput() + assert.Contains(t, output, "Unknown tool specified") + assert.Contains(t, output, "invalid-tool") + assert.Contains(t, output, "not-exists") + + // Verify that valid tools were still registered + assert.Contains(t, output, "Running KAgent Tools Server") + assert.Contains(t, output, "k8s") + + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestConcurrentToolExecution tests concurrent tool execution +func TestConcurrentToolExecution(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8088, + Tools: []string{"utils", "k8s"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Create multiple concurrent requests + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + require.NoError(t, err, "Concurrent request %d should succeed", id) + assert.Equal(t, http.StatusOK, resp.StatusCode) + resp.Body.Close() + }(i) + } + + wg.Wait() + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestServerErrorHandling tests server's error handling capabilities +func TestServerErrorHandling(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8089, + Tools: []string{"utils"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Test malformed request + req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/nonexistent", config.Port), strings.NewReader("invalid json")) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + resp.Body.Close() + + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestServerMetricsEndpoint tests the metrics endpoint functionality +func TestServerMetricsEndpoint(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8090, + Tools: []string{"utils"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Test metrics endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/metrics", config.Port)) + require.NoError(t, err, "Metrics endpoint should be accessible") + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // Read and verify metrics content + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + resp.Body.Close() + + metricsContent := string(body) + assert.Contains(t, metricsContent, "go_") + assert.Contains(t, metricsContent, "process_") + + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} + +// TestToolSpecificFunctionality tests specific functionality of registered tools +func TestToolSpecificFunctionality(t *testing.T) { + ctx := context.Background() + + config := TestServerConfig{ + Port: 8091, + Tools: []string{"utils", "k8s"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + require.NoError(t, err, "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Test utils tool endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + resp.Body.Close() + + // Verify response format matches expected OK response + assert.Equal(t, "OK", string(body), "Should return OK response") + + err = server.Stop() + require.NoError(t, err, "Server should stop gracefully") +} diff --git a/go.mod b/go.mod index 08a9e047..220ea7f2 100644 --- a/go.mod +++ b/go.mod @@ -1,67 +1,47 @@ module github.com/kagent-dev/tools -go 1.24.4 +go 1.24.5 require ( github.com/go-logr/logr v1.4.3 github.com/go-logr/stdr v1.2.2 github.com/joho/godotenv v1.5.1 - github.com/kagent-dev/kagent/go v0.0.0-20250707014726-aa7651a0e4e3 github.com/mark3labs/mcp-go v0.32.0 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 github.com/tmc/langchaingo v0.1.13 go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 go.opentelemetry.io/otel/metric v1.37.0 + go.opentelemetry.io/otel/sdk v1.37.0 + go.opentelemetry.io/otel/trace v1.37.0 ) require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.10.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/fxamacker/cbor/v2 v2.8.0 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.9.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.6 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/cast v1.9.2 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/x448/float16 v0.8.4 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.0 // indirect golang.org/x/net v0.41.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.26.0 // indirect - golang.org/x/time v0.12.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.2 // indirect - k8s.io/apimachinery v0.33.2 // indirect - k8s.io/client-go v0.33.2 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 15455ac7..3a1cf490 100644 --- a/go.sum +++ b/go.sum @@ -1,77 +1,40 @@ +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= -github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= -github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kagent-dev/kagent/go v0.0.0-20250707014726-aa7651a0e4e3 h1:B5EkhSmYMG6bgn7DTsOfhal8sl1MmhjixSXP1PP/jNw= -github.com/kagent-dev/kagent/go v0.0.0-20250707014726-aa7651a0e4e3/go.mod h1:hwTH7K+UkePRxA6DhXOXavNyXRK3nPmvipA07DSRUxI= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8= github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -83,98 +46,54 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 h1:jBpDk4HAUsrnVO1FsfCfCOTEc/MkInJmvfCHYLFiT80= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0/go.mod h1:H9LUIM1daaeZaz91vZcfeM0fejXPmgCYE8ZhzqfJuiU= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= -k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= -k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= -k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= -k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a h1:ZV3Zr+/7s7aVbjNGICQt+ppKWsF1tehxggNfbM7XnG8= -k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 00000000..4c2c1056 --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,545 @@ +package cache + +import ( + "context" + "fmt" + "sync" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + "github.com/kagent-dev/tools/internal/logger" + "github.com/kagent-dev/tools/internal/telemetry" +) + +// CacheType represents the type of cache using enum pattern +type CacheType int + +const ( + CacheTypeKubernetes CacheType = iota + CacheTypeCommand + CacheTypeHelm + CacheTypeIstio +) + +// String returns the string representation of CacheType +func (ct CacheType) String() string { + switch ct { + case CacheTypeKubernetes: + return "kubernetes" + case CacheTypeCommand: + return "command" + case CacheTypeHelm: + return "helm" + case CacheTypeIstio: + return "istio" + default: + return "unknown" + } +} + +// Command to cache type mapping +var commandToCacheType = map[string]CacheType{ + "kubectl": CacheTypeKubernetes, + "helm": CacheTypeHelm, + "istioctl": CacheTypeIstio, + "cilium": CacheTypeCommand, // Use command cache for cilium + "argo": CacheTypeCommand, // Use command cache for argo +} + +// CacheEntry represents a cached item with TTL +type CacheEntry[T any] struct { + Value T + CreatedAt time.Time + ExpiresAt time.Time + AccessedAt time.Time + AccessCount int64 +} + +// IsExpired checks if the cache entry has expired +func (e *CacheEntry[T]) IsExpired() bool { + return time.Now().After(e.ExpiresAt) +} + +// Cache is a thread-safe cache with TTL support +type Cache[T any] struct { + mu sync.RWMutex + data map[string]*CacheEntry[T] + name string + defaultTTL time.Duration + maxSize int + cleanupInterval time.Duration + stopCleanup chan struct{} + + // Metrics + hits metric.Int64Counter + misses metric.Int64Counter + evictions metric.Int64Counter + size metric.Int64UpDownCounter +} + +// NewCache creates a new cache with specified configuration and name +func NewCache[T any](name string, defaultTTL time.Duration, maxSize int, cleanupInterval time.Duration) *Cache[T] { + meter := otel.Meter(fmt.Sprintf("kagent-tools/cache/%s", name)) + + // Create metrics with cache name as a label + hits, _ := meter.Int64Counter( + "cache_hits_total", + metric.WithDescription("Total number of cache hits"), + ) + + misses, _ := meter.Int64Counter( + "cache_misses_total", + metric.WithDescription("Total number of cache misses"), + ) + + evictions, _ := meter.Int64Counter( + "cache_evictions_total", + metric.WithDescription("Total number of cache evictions"), + ) + + size, _ := meter.Int64UpDownCounter( + "cache_size", + metric.WithDescription("Current number of items in cache"), + ) + + cache := &Cache[T]{ + data: make(map[string]*CacheEntry[T]), + name: name, + defaultTTL: defaultTTL, + maxSize: maxSize, + cleanupInterval: cleanupInterval, + stopCleanup: make(chan struct{}), + hits: hits, + misses: misses, + evictions: evictions, + size: size, + } + + // Start background cleanup + go cache.cleanupExpired() + + return cache +} + +// Get retrieves a value from the cache +func (c *Cache[T]) Get(key string) (T, bool) { + ctx := context.Background() + _, span := telemetry.StartSpan(ctx, "cache.get", + attribute.String("cache.name", c.name), + attribute.String("cache.key", key), + ) + defer span.End() + + c.mu.RLock() + defer c.mu.RUnlock() + + entry, exists := c.data[key] + if !exists { + var zero T + c.recordMiss(key) + telemetry.AddEvent(span, "cache.miss", + attribute.String("cache.result", "miss"), + ) + span.SetAttributes(attribute.String("cache.result", "miss")) + return zero, false + } + + if entry.IsExpired() { + var zero T + c.recordMiss(key) + telemetry.AddEvent(span, "cache.miss", + attribute.String("cache.result", "miss"), + attribute.String("cache.miss_reason", "expired"), + ) + span.SetAttributes( + attribute.String("cache.result", "miss"), + attribute.String("cache.miss_reason", "expired"), + ) + return zero, false + } + + // Update access time and count + entry.AccessedAt = time.Now() + entry.AccessCount++ + + c.recordHit(key) + telemetry.AddEvent(span, "cache.hit", + attribute.String("cache.result", "hit"), + attribute.Int64("cache.access_count", entry.AccessCount), + ) + span.SetAttributes( + attribute.String("cache.result", "hit"), + attribute.Int64("cache.access_count", entry.AccessCount), + ) + + logger.Get().Debug("Cache hit", "key", key, "access_count", entry.AccessCount) + return entry.Value, true +} + +// Set stores a value in the cache with default TTL +func (c *Cache[T]) Set(key string, value T) { + c.SetWithTTL(key, value, c.defaultTTL) +} + +// SetWithTTL stores a value in the cache with specified TTL +func (c *Cache[T]) SetWithTTL(key string, value T, ttl time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + + now := time.Now() + + // Check if we need to evict items to make room + if len(c.data) >= c.maxSize { + c.evictLRU() + } + + entry := &CacheEntry[T]{ + Value: value, + CreatedAt: now, + ExpiresAt: now.Add(ttl), + AccessedAt: now, + AccessCount: 1, + } + + // Check if key already exists + if _, exists := c.data[key]; !exists { + c.size.Add(context.Background(), 1) + } + + c.data[key] = entry + + logger.Get().Debug("Cache set", "key", key, "ttl", ttl) +} + +// Delete removes a value from the cache +func (c *Cache[T]) Delete(key string) { + c.mu.Lock() + defer c.mu.Unlock() + + if _, exists := c.data[key]; exists { + delete(c.data, key) + c.size.Add(context.Background(), -1) + logger.Get().Debug("Cache delete", "key", key) + } +} + +// Clear removes all items from the cache +func (c *Cache[T]) Clear() { + c.mu.Lock() + defer c.mu.Unlock() + + count := len(c.data) + c.data = make(map[string]*CacheEntry[T]) + c.size.Add(context.Background(), -int64(count)) + + logger.Get().Info("Cache cleared", "items_removed", count) +} + +// Size returns the current number of items in the cache +func (c *Cache[T]) Size() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.data) +} + +// Name returns the name of the cache +func (c *Cache[T]) Name() string { + return c.name +} + +// Stats returns cache statistics +func (c *Cache[T]) Stats() CacheStats { + c.mu.RLock() + defer c.mu.RUnlock() + + stats := CacheStats{ + Size: len(c.data), + MaxSize: c.maxSize, + Expired: 0, + Oldest: time.Now(), + Newest: time.Time{}, + } + + for _, entry := range c.data { + if entry.IsExpired() { + stats.Expired++ + } + + if entry.CreatedAt.Before(stats.Oldest) { + stats.Oldest = entry.CreatedAt + } + + if entry.CreatedAt.After(stats.Newest) { + stats.Newest = entry.CreatedAt + } + } + + return stats +} + +// CacheStats represents cache statistics +type CacheStats struct { + Size int `json:"size"` + MaxSize int `json:"max_size"` + Expired int `json:"expired"` + Oldest time.Time `json:"oldest"` + Newest time.Time `json:"newest"` +} + +// cleanupExpired removes expired entries from the cache +func (c *Cache[T]) cleanupExpired() { + ticker := time.NewTicker(c.cleanupInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + c.performCleanup() + case <-c.stopCleanup: + return + } + } +} + +// performCleanup removes expired entries +func (c *Cache[T]) performCleanup() { + c.mu.Lock() + defer c.mu.Unlock() + + keysToDelete := make([]string, 0) + + for key, entry := range c.data { + if entry.IsExpired() { + keysToDelete = append(keysToDelete, key) + } + } + + if len(keysToDelete) > 0 { + for _, key := range keysToDelete { + delete(c.data, key) + c.evictions.Add(context.Background(), 1) + } + + c.size.Add(context.Background(), -int64(len(keysToDelete))) + logger.Get().Debug("Cache cleanup", "expired_items", len(keysToDelete)) + } +} + +// evictLRU removes the least recently used item +func (c *Cache[T]) evictLRU() { + var oldestKey string + var oldestTime time.Time = time.Now() + + for key, entry := range c.data { + if entry.AccessedAt.Before(oldestTime) { + oldestTime = entry.AccessedAt + oldestKey = key + } + } + + if oldestKey != "" { + delete(c.data, oldestKey) + c.evictions.Add(context.Background(), 1) + c.size.Add(context.Background(), -1) + logger.Get().Debug("Cache LRU eviction", "key", oldestKey) + } +} + +// recordHit records a cache hit +func (c *Cache[T]) recordHit(key string) { + c.hits.Add(context.Background(), 1, metric.WithAttributes( + attribute.String("cache.key", key), + attribute.String("cache.result", "hit"), + attribute.String("cache.name", c.name), + )) +} + +// recordMiss records a cache miss +func (c *Cache[T]) recordMiss(key string) { + c.misses.Add(context.Background(), 1, metric.WithAttributes( + attribute.String("cache.key", key), + attribute.String("cache.result", "miss"), + attribute.String("cache.name", c.name), + )) +} + +// Close stops the cache cleanup goroutine +func (c *Cache[T]) Close() { + close(c.stopCleanup) +} + +// InvalidateByType clears the entire cache for a specific cache type +func InvalidateByType(cacheType CacheType) { + ctx := context.Background() + _, span := telemetry.StartSpan(ctx, "cache.invalidate", + attribute.String("cache.type", cacheType.String()), + attribute.String("cache.operation", "invalidate"), + ) + defer span.End() + + InitCaches() + if cache, exists := cacheRegistry[cacheType]; exists { + oldSize := cache.Size() + cache.Clear() + + telemetry.AddEvent(span, "cache.invalidated", + attribute.String("cache.name", cache.name), + attribute.Int("cache.items_cleared", oldSize), + ) + span.SetAttributes( + attribute.String("cache.name", cache.name), + attribute.Int("cache.items_cleared", oldSize), + ) + telemetry.RecordSuccess(span, "Cache invalidated successfully") + + logger.Get().Info("Cache invalidated", "cache_type", cacheType.String(), "reason", "modification_command", "items_cleared", oldSize) + } else { + telemetry.RecordError(span, fmt.Errorf("cache type not found: %s", cacheType.String()), "Cache type not found") + } +} + +// InvalidateKubernetesCache clears the Kubernetes cache +func InvalidateKubernetesCache() { + InvalidateByType(CacheTypeKubernetes) +} + +// InvalidateHelmCache clears the Helm cache +func InvalidateHelmCache() { + InvalidateByType(CacheTypeHelm) +} + +// InvalidateIstioCache clears the Istio cache +func InvalidateIstioCache() { + InvalidateByType(CacheTypeIstio) +} + +// InvalidateCommandCache clears the Command cache +func InvalidateCommandCache() { + InvalidateByType(CacheTypeCommand) +} + +// InvalidateCacheForCommand invalidates the appropriate cache based on command type +func InvalidateCacheForCommand(command string) { + if cacheType, exists := commandToCacheType[command]; exists { + InvalidateByType(cacheType) + } else { + // Default to command cache for unknown commands + InvalidateCommandCache() + } +} + +// Global cache instances for different use cases +var ( + // cacheRegistry holds all cache instances by type + cacheRegistry = make(map[CacheType]*Cache[string]) + once sync.Once +) + +// InitCaches initializes all global cache instances +func InitCaches() { + once.Do(func() { + // Initialize caches with optimized TTL values based on use case + // Kubernetes: 45s - K8s resources change frequently, users expect fresh data + cacheRegistry[CacheTypeKubernetes] = NewCache[string](CacheTypeKubernetes.String(), 45*time.Second, 1000, 1*time.Minute) + + // Istio: 1m - Service mesh config more stable than pods, but proxy status can change + cacheRegistry[CacheTypeIstio] = NewCache[string](CacheTypeIstio.String(), 1*time.Minute, 500, 1*time.Minute) + + // Helm: 2m - Releases change less frequently, chart info is stable + cacheRegistry[CacheTypeHelm] = NewCache[string](CacheTypeHelm.String(), 2*time.Minute, 300, 2*time.Minute) + + // Command: 3m - General CLI commands have stable output, status commands don't change rapidly + cacheRegistry[CacheTypeCommand] = NewCache[string](CacheTypeCommand.String(), 3*time.Minute, 200, 1*time.Minute) + + logger.Get().Info("Caches initialized") + }) +} + +// GetCacheByType returns a cache instance by cache type +func GetCacheByType(cacheType CacheType) *Cache[string] { + InitCaches() + if cache, exists := cacheRegistry[cacheType]; exists { + return cache + } + // Fallback to command cache if type not found + return cacheRegistry[CacheTypeCommand] +} + +// GetCacheByCommand returns a cache instance based on the command name +func GetCacheByCommand(command string) *Cache[string] { + InitCaches() + if cacheType, exists := commandToCacheType[command]; exists { + return GetCacheByType(cacheType) + } + // Default to command cache for unknown commands + return GetCacheByType(CacheTypeCommand) +} + +// CacheKey generates a consistent cache key from components +func CacheKey(components ...string) string { + result := "" + for i, component := range components { + if i > 0 { + result += ":" + } + result += component + } + return result +} + +// CacheResult is a helper function to cache the result of a function +func CacheResult[T any](cache *Cache[T], key string, ttl time.Duration, fn func() (T, error)) (T, error) { + ctx := context.Background() + _, span := telemetry.StartSpan(ctx, "cache.result", + attribute.String("cache.name", cache.name), + attribute.String("cache.key", key), + attribute.String("cache.ttl", ttl.String()), + ) + defer span.End() + + var zero T + + // Try to get from cache first + if cachedResult, found := cache.Get(key); found { + telemetry.AddEvent(span, "cache.result.hit", + attribute.String("cache.operation", "get"), + attribute.String("cache.result", "hit"), + ) + span.SetAttributes( + attribute.String("cache.operation", "get"), + attribute.String("cache.result", "hit"), + ) + telemetry.RecordSuccess(span, "Cache hit - returning cached result") + return cachedResult, nil + } + + // Not in cache, execute function + telemetry.AddEvent(span, "cache.result.miss", + attribute.String("cache.operation", "compute"), + attribute.String("cache.result", "miss"), + ) + span.SetAttributes( + attribute.String("cache.operation", "compute"), + attribute.String("cache.result", "miss"), + ) + + result, err := fn() + if err != nil { + telemetry.RecordError(span, err, "Function execution failed") + return zero, err + } + + // Store in cache + cache.SetWithTTL(key, result, ttl) + + telemetry.AddEvent(span, "cache.result.stored", + attribute.String("cache.operation", "set"), + ) + span.SetAttributes(attribute.String("cache.operation", "set")) + telemetry.RecordSuccess(span, "Function executed and result cached") + + return result, nil +} diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go new file mode 100644 index 00000000..cc7cf641 --- /dev/null +++ b/internal/cache/cache_test.go @@ -0,0 +1,488 @@ +package cache + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNewCache(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 100, 10*time.Second) + + if cache.defaultTTL != 1*time.Minute { + t.Errorf("Expected default TTL of 1 minute, got %v", cache.defaultTTL) + } + + if cache.maxSize != 100 { + t.Errorf("Expected max size of 100, got %d", cache.maxSize) + } + + if cache.cleanupInterval != 10*time.Second { + t.Errorf("Expected cleanup interval of 10 seconds, got %v", cache.cleanupInterval) + } + + if cache.name != "test-cache" { + t.Errorf("Expected cache name 'test-cache', got %s", cache.name) + } + + cache.Close() +} + +func TestCacheName(t *testing.T) { + cache := NewCache[string]("my-test-cache", 1*time.Minute, 100, 10*time.Second) + defer cache.Close() + + if cache.Name() != "my-test-cache" { + t.Errorf("Expected cache name 'my-test-cache', got %s", cache.Name()) + } +} + +func TestCacheSetAndGet(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 100, 10*time.Second) + defer cache.Close() + + // Test set and get + cache.Set("key1", "value1") + value, found := cache.Get("key1") + + if !found { + t.Error("Expected to find key1") + } + + if value != "value1" { + t.Errorf("Expected value1, got %v", value) + } +} + +func TestCacheSetWithTTL(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 100, 10*time.Second) + defer cache.Close() + + // Test set with custom TTL + cache.SetWithTTL("key1", "value1", 100*time.Millisecond) + + // Should be found immediately + value, found := cache.Get("key1") + if !found { + t.Error("Expected to find key1") + } + if value != "value1" { + t.Errorf("Expected value1, got %v", value) + } + + // Wait for expiration + time.Sleep(150 * time.Millisecond) + + // Should not be found after expiration + _, found = cache.Get("key1") + if found { + t.Error("Expected key1 to be expired") + } +} + +func TestCacheDelete(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 100, 10*time.Second) + defer cache.Close() + + cache.Set("key1", "value1") + cache.Delete("key1") + + _, found := cache.Get("key1") + if found { + t.Error("Expected key1 to be deleted") + } +} + +func TestCacheClear(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 100, 10*time.Second) + defer cache.Close() + + cache.Set("key1", "value1") + cache.Set("key2", "value2") + + if cache.Size() != 2 { + t.Errorf("Expected size 2, got %d", cache.Size()) + } + + cache.Clear() + + if cache.Size() != 0 { + t.Errorf("Expected size 0 after clear, got %d", cache.Size()) + } +} + +func TestCacheEviction(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 2, 10*time.Second) // Small cache + defer cache.Close() + + // Fill cache to capacity + cache.Set("key1", "value1") + cache.Set("key2", "value2") + + // Add one more item - should evict LRU + cache.Set("key3", "value3") + + // key1 should be evicted (oldest) + _, found := cache.Get("key1") + if found { + t.Error("Expected key1 to be evicted") + } + + // key2 and key3 should still be there + _, found = cache.Get("key2") + if !found { + t.Error("Expected key2 to be present") + } + + _, found = cache.Get("key3") + if !found { + t.Error("Expected key3 to be present") + } +} + +func TestCacheExpiration(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 100, 50*time.Millisecond) // Fast cleanup + defer cache.Close() + + // Set item with short TTL + cache.SetWithTTL("key1", "value1", 100*time.Millisecond) + + // Wait for cleanup to run + time.Sleep(200 * time.Millisecond) + + // Item should be cleaned up + _, found := cache.Get("key1") + if found { + t.Error("Expected key1 to be cleaned up") + } +} + +func TestCacheStats(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 100, 10*time.Second) + defer cache.Close() + + cache.Set("key1", "value1") + cache.Set("key2", "value2") + + stats := cache.Stats() + + if stats.Size != 2 { + t.Errorf("Expected stats size 2, got %d", stats.Size) + } + + if stats.MaxSize != 100 { + t.Errorf("Expected stats max size 100, got %d", stats.MaxSize) + } + + if stats.Expired != 0 { + t.Errorf("Expected 0 expired items, got %d", stats.Expired) + } +} + +func TestCacheKey(t *testing.T) { + tests := []struct { + name string + components []string + expected string + }{ + {"single component", []string{"key1"}, "key1"}, + {"multiple components", []string{"key1", "key2", "key3"}, "key1:key2:key3"}, + {"empty components", []string{}, ""}, + {"empty string component", []string{"key1", "", "key3"}, "key1::key3"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := CacheKey(tt.components...) + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestCacheResult(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 100, 10*time.Second) + defer cache.Close() + + callCount := 0 + testFunction := func() (string, error) { + callCount++ + return "result", nil + } + + // First call should execute function + result, err := CacheResult(cache, "test-key", 1*time.Minute, testFunction) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "result" { + t.Errorf("Expected 'result', got %q", result) + } + if callCount != 1 { + t.Errorf("Expected function to be called once, got %d", callCount) + } + + // Second call should use cache + result, err = CacheResult(cache, "test-key", 1*time.Minute, testFunction) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "result" { + t.Errorf("Expected 'result', got %q", result) + } + if callCount != 1 { + t.Errorf("Expected function to be called once (cached), got %d", callCount) + } +} + +func TestCacheResultWithError(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 100, 10*time.Second) + defer cache.Close() + + testFunction := func() (string, error) { + return "", &testError{message: "test error"} + } + + result, err := CacheResult(cache, "test-key", 1*time.Minute, testFunction) + if err == nil { + t.Error("Expected error") + } + if result != "" { + t.Errorf("Expected empty result, got %q", result) + } + + // Check that error result is not cached + _, found := cache.Get("test-key") + if found { + t.Error("Expected error result not to be cached") + } +} + +func TestCacheInitialization(t *testing.T) { + // Test that all cache types are properly initialized + types := []CacheType{ + CacheTypeKubernetes, + CacheTypeCommand, + CacheTypeHelm, + CacheTypeIstio, + } + + for _, cacheType := range types { + t.Run(cacheType.String(), func(t *testing.T) { + cache := GetCacheByType(cacheType) + if cache == nil { + t.Errorf("Expected cache for type %s to be initialized", cacheType.String()) + } + if cache.Name() != cacheType.String() { + t.Errorf("Expected cache name %s, got %s", cacheType.String(), cache.Name()) + } + }) + } +} + +func TestCacheEntry(t *testing.T) { + now := time.Now() + entry := &CacheEntry[string]{ + Value: "test", + CreatedAt: now, + ExpiresAt: now.Add(1 * time.Minute), + AccessedAt: now, + AccessCount: 1, + } + + // Should not be expired + if entry.IsExpired() { + t.Error("Expected entry not to be expired") + } + + // Make it expired + entry.ExpiresAt = now.Add(-1 * time.Minute) + + // Should be expired + if !entry.IsExpired() { + t.Error("Expected entry to be expired") + } +} + +func TestCachePerformCleanup(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 100, 10*time.Second) + defer cache.Close() + + // Add expired item + cache.SetWithTTL("expired", "value", -1*time.Minute) + + // Add valid item + cache.Set("valid", "value") + + // Perform cleanup + cache.performCleanup() + + // Expired item should be removed + _, found := cache.Get("expired") + if found { + t.Error("Expected expired item to be removed") + } + + // Valid item should remain + _, found = cache.Get("valid") + if !found { + t.Error("Expected valid item to remain") + } +} + +func TestCacheConcurrency(t *testing.T) { + cache := NewCache[string]("test-cache", 1*time.Minute, 1000, 10*time.Second) + defer cache.Close() + + // Test concurrent operations + done := make(chan bool) + + // Writer goroutine + go func() { + for i := 0; i < 100; i++ { + cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i)) + } + done <- true + }() + + // Reader goroutine + go func() { + for i := 0; i < 100; i++ { + cache.Get(fmt.Sprintf("key%d", i)) + } + done <- true + }() + + // Wait for both goroutines + <-done + <-done + + // Cache should have items + if cache.Size() == 0 { + t.Error("Expected cache to have items") + } +} + +// Helper types for testing +type testError struct { + message string +} + +func (e *testError) Error() string { + return e.message +} + +func TestCacheTypeString(t *testing.T) { + tests := []struct { + cacheType CacheType + expected string + }{ + {CacheTypeKubernetes, "kubernetes"}, + {CacheTypeCommand, "command"}, + {CacheTypeHelm, "helm"}, + {CacheTypeIstio, "istio"}, + {CacheType(999), "unknown"}, // Test unknown type + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + result := tt.cacheType.String() + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestGetCacheByType(t *testing.T) { + // Test all valid cache types + types := []CacheType{ + CacheTypeKubernetes, + CacheTypeCommand, + CacheTypeHelm, + CacheTypeIstio, + } + + for _, cacheType := range types { + t.Run(cacheType.String(), func(t *testing.T) { + cache := GetCacheByType(cacheType) + if cache == nil { + t.Errorf("Expected cache for type %s, got nil", cacheType.String()) + } + if cache.Name() != cacheType.String() { + t.Errorf("Expected cache name %s, got %s", cacheType.String(), cache.Name()) + } + }) + } +} + +func TestGetCacheByCommand(t *testing.T) { + tests := []struct { + command string + expectedType CacheType + }{ + {"kubectl", CacheTypeKubernetes}, + {"helm", CacheTypeHelm}, + {"istioctl", CacheTypeIstio}, + {"cilium", CacheTypeCommand}, + {"argo", CacheTypeCommand}, + {"unknown-command", CacheTypeCommand}, // Should default to command cache + } + + for _, tt := range tests { + t.Run(tt.command, func(t *testing.T) { + cache := GetCacheByCommand(tt.command) + if cache == nil { + t.Errorf("Expected cache for command %s, got nil", tt.command) + } + if cache.Name() != tt.expectedType.String() { + t.Errorf("Expected cache name %s for command %s, got %s", + tt.expectedType.String(), tt.command, cache.Name()) + } + }) + } +} + +func TestCacheOTelTracing(t *testing.T) { + // This test verifies that OTEL tracing calls don't panic + // The actual tracing verification would require setting up an OTEL test environment + cache := NewCache[string]("test-tracing", 1*time.Minute, 10, 5*time.Minute) + defer cache.Close() + + // Test cache miss with tracing + _, found := cache.Get("missing-key") + assert.False(t, found) + + // Test cache hit with tracing + cache.Set("test-key", "test-value") + value, found := cache.Get("test-key") + assert.True(t, found) + assert.Equal(t, "test-value", value) + + // Test CacheResult with tracing + callCount := 0 + result, err := CacheResult(cache, "result-key", 1*time.Minute, func() (string, error) { + callCount++ + return "computed-value", nil + }) + assert.NoError(t, err) + assert.Equal(t, "computed-value", result) + assert.Equal(t, 1, callCount) + + // Test cache hit on second call + result2, err := CacheResult(cache, "result-key", 1*time.Minute, func() (string, error) { + callCount++ + return "computed-value", nil + }) + assert.NoError(t, err) + assert.Equal(t, "computed-value", result2) + assert.Equal(t, 1, callCount) // Should not increment due to cache hit + + // Test cache invalidation with tracing + oldSize := cache.Size() + InvalidateByType(CacheTypeCommand) + assert.True(t, oldSize > 0) // Verify we had items to clear +} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go new file mode 100644 index 00000000..30610061 --- /dev/null +++ b/internal/cmd/cmd.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "context" + "os/exec" + "time" + + "github.com/kagent-dev/tools/internal/logger" +) + +// ShellExecutor defines the interface for executing shell commands +type ShellExecutor interface { + Exec(ctx context.Context, command string, args ...string) (output []byte, err error) +} + +// DefaultShellExecutor implements ShellExecutor using os/exec +type DefaultShellExecutor struct{} + +// Exec executes a command using os/exec.CommandContext +func (e *DefaultShellExecutor) Exec(ctx context.Context, command string, args ...string) ([]byte, error) { + log := logger.WithContext(ctx) + startTime := time.Now() + + log.Info("executing command", + "command", command, + "args", args, + ) + + cmd := exec.CommandContext(ctx, command, args...) + output, err := cmd.CombinedOutput() + + duration := time.Since(startTime) + + if err != nil { + log.Error("command execution failed", + "command", command, + "args", args, + "error", err, + "output", string(output), + "duration", duration.Seconds(), + ) + } else { + log.Info("command execution successful", + "command", command, + "args", args, + "duration", duration.Seconds(), + ) + } + + return output, err +} + +// Context key for shell executor injection +type contextKey string + +const shellExecutorKey contextKey = "shellExecutor" + +// WithShellExecutor returns a context with the given shell executor +func WithShellExecutor(ctx context.Context, executor ShellExecutor) context.Context { + return context.WithValue(ctx, shellExecutorKey, executor) +} + +// GetShellExecutor retrieves the shell executor from context, or returns default +func GetShellExecutor(ctx context.Context) ShellExecutor { + if executor, ok := ctx.Value(shellExecutorKey).(ShellExecutor); ok { + return executor + } + return &DefaultShellExecutor{} +} diff --git a/internal/cmd/cmd_test.go b/internal/cmd/cmd_test.go new file mode 100644 index 00000000..f902d4c4 --- /dev/null +++ b/internal/cmd/cmd_test.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultShellExecutor(t *testing.T) { + executor := &DefaultShellExecutor{} + + // Test successful command + output, err := executor.Exec(context.Background(), "echo", "hello") + assert.NoError(t, err) + assert.Equal(t, "hello\n", string(output)) + + // Test command with error + _, err = executor.Exec(context.Background(), "nonexistent-command") + assert.Error(t, err) +} + +func TestMockShellExecutor(t *testing.T) { + mock := NewMockShellExecutor() + + t.Run("unmocked command returns error", func(t *testing.T) { + _, err := mock.Exec(context.Background(), "unmocked", "command") + assert.Error(t, err) + assert.Contains(t, err.Error(), "no mock found for command") + }) + + t.Run("mocked command returns expected result", func(t *testing.T) { + expectedOutput := "mocked output" + mock.AddCommandString("kubectl", []string{"get", "pods"}, expectedOutput, nil) + + output, err := mock.Exec(context.Background(), "kubectl", "get", "pods") + assert.NoError(t, err) + assert.Equal(t, expectedOutput, string(output)) + }) +} + +func TestContextShellExecutor(t *testing.T) { + t.Run("default executor when no context value", func(t *testing.T) { + ctx := context.Background() + executor := GetShellExecutor(ctx) + + _, ok := executor.(*DefaultShellExecutor) + assert.True(t, ok, "should return DefaultShellExecutor when no context value") + }) + + t.Run("mock executor from context", func(t *testing.T) { + mock := NewMockShellExecutor() + ctx := WithShellExecutor(context.Background(), mock) + + executor := GetShellExecutor(ctx) + assert.Equal(t, mock, executor, "should return the mock executor from context") + }) +} diff --git a/internal/cmd/mock.go b/internal/cmd/mock.go new file mode 100644 index 00000000..3f13c47b --- /dev/null +++ b/internal/cmd/mock.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + "sync" +) + +// MockCall represents a recorded command execution for testing +type MockCall struct { + Command string + Args []string +} + +// MockShellExecutor is a mock implementation of ShellExecutor for testing +type MockShellExecutor struct { + mu sync.Mutex + callLog []MockCall + commandMocks map[string]map[string]struct { + output string + err error + } + partialMatchers []struct { + command string + args []string + output string + err error + } +} + +// NewMockShellExecutor creates a new mock shell executor +func NewMockShellExecutor() *MockShellExecutor { + return &MockShellExecutor{ + commandMocks: make(map[string]map[string]struct { + output string + err error + }), + } +} + +// AddCommandString mocks a command with specific arguments and a string output +func (m *MockShellExecutor) AddCommandString(command string, args []string, output string, err error) { + m.mu.Lock() + defer m.mu.Unlock() + + argsKey := strings.Join(args, " ") + if _, ok := m.commandMocks[command]; !ok { + m.commandMocks[command] = make(map[string]struct { + output string + err error + }) + } + m.commandMocks[command][argsKey] = struct { + output string + err error + }{output, err} +} + +// AddPartialMatcherString mocks a command with partial argument matching +func (m *MockShellExecutor) AddPartialMatcherString(command string, args []string, output string, err error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.partialMatchers = append(m.partialMatchers, struct { + command string + args []string + output string + err error + }{command, args, output, err}) +} + +// Exec records the call and returns a mocked output or error +func (m *MockShellExecutor) Exec(ctx context.Context, command string, args ...string) ([]byte, error) { + m.mu.Lock() + defer m.mu.Unlock() + + m.callLog = append(m.callLog, MockCall{Command: command, Args: args}) + + // Check for exact match first + argsKey := strings.Join(args, " ") + if mocks, ok := m.commandMocks[command]; ok { + if mock, ok := mocks[argsKey]; ok { + return []byte(mock.output), mock.err + } + } + + // Check for partial match + for _, matcher := range m.partialMatchers { + if matcher.command == command && argsContain(args, matcher.args) { + return []byte(matcher.output), matcher.err + } + } + + return nil, fmt.Errorf("no mock found for command: %s %v", command, args) +} + +// GetCallLog returns the history of commands executed +func (m *MockShellExecutor) GetCallLog() []MockCall { + m.mu.Lock() + defer m.mu.Unlock() + return m.callLog +} + +// argsContain checks if all elements of subset are in set +func argsContain(set, subset []string) bool { + for _, sub := range subset { + found := false + for _, s := range set { + if strings.Contains(s, sub) { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/internal/commands/builder.go b/internal/commands/builder.go new file mode 100644 index 00000000..a6bd8e6d --- /dev/null +++ b/internal/commands/builder.go @@ -0,0 +1,747 @@ +package commands + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/kagent-dev/tools/internal/cache" + "github.com/kagent-dev/tools/internal/cmd" + "github.com/kagent-dev/tools/internal/errors" + "github.com/kagent-dev/tools/internal/logger" + "github.com/kagent-dev/tools/internal/security" + "github.com/kagent-dev/tools/internal/telemetry" + "go.opentelemetry.io/otel/attribute" +) + +// CommandBuilder provides a fluent interface for building CLI commands +type CommandBuilder struct { + command string + args []string + namespace string + context string + kubeconfig string + output string + labels map[string]string + annotations map[string]string + timeout time.Duration + dryRun bool + force bool + wait bool + validate bool + cached bool + cacheTTL time.Duration + cacheKey string +} + +// NewCommandBuilder creates a new command builder +func NewCommandBuilder(command string) *CommandBuilder { + return &CommandBuilder{ + command: command, + args: make([]string, 0), + labels: make(map[string]string), + annotations: make(map[string]string), + timeout: 30 * time.Second, + validate: true, + cacheTTL: 5 * time.Minute, + } +} + +// KubectlBuilder creates a kubectl command builder +func KubectlBuilder() *CommandBuilder { + return NewCommandBuilder("kubectl") +} + +// HelmBuilder creates a helm command builder +func HelmBuilder() *CommandBuilder { + return NewCommandBuilder("helm") +} + +// IstioCtlBuilder creates an istioctl command builder +func IstioCtlBuilder() *CommandBuilder { + return NewCommandBuilder("istioctl") +} + +// CiliumBuilder creates a cilium command builder +func CiliumBuilder() *CommandBuilder { + return NewCommandBuilder("cilium") +} + +// ArgoRolloutsBuilder creates an argo rollouts command builder +func ArgoRolloutsBuilder() *CommandBuilder { + return NewCommandBuilder("kubectl").WithArgs("argo", "rollouts") +} + +// WithArgs adds arguments to the command +func (cb *CommandBuilder) WithArgs(args ...string) *CommandBuilder { + cb.args = append(cb.args, args...) + return cb +} + +// WithNamespace sets the namespace +func (cb *CommandBuilder) WithNamespace(namespace string) *CommandBuilder { + if err := security.ValidateNamespace(namespace); err != nil { + logger.Get().Error("Invalid namespace", "namespace", namespace, "error", err) + return cb + } + cb.namespace = namespace + return cb +} + +// WithContext sets the Kubernetes context +func (cb *CommandBuilder) WithContext(context string) *CommandBuilder { + if err := security.ValidateCommandInput(context); err != nil { + logger.Get().Error("Invalid context", "context", context, "error", err) + return cb + } + cb.context = context + return cb +} + +// WithKubeconfig sets the kubeconfig file +func (cb *CommandBuilder) WithKubeconfig(kubeconfig string) *CommandBuilder { + if err := security.ValidateFilePath(kubeconfig); err != nil { + logger.Get().Error("Invalid kubeconfig path", "kubeconfig", kubeconfig, "error", err) + return cb + } + cb.kubeconfig = kubeconfig + return cb +} + +// WithOutput sets the output format +func (cb *CommandBuilder) WithOutput(output string) *CommandBuilder { + validOutputs := []string{"json", "yaml", "wide", "name", "custom-columns", "custom-columns-file", "go-template", "go-template-file", "jsonpath", "jsonpath-file"} + + valid := false + for _, validOutput := range validOutputs { + if output == validOutput { + valid = true + break + } + } + + if !valid { + logger.Get().Error("Invalid output format", "output", output) + return cb + } + + cb.output = output + return cb +} + +// WithLabel adds a label selector +func (cb *CommandBuilder) WithLabel(key, value string) *CommandBuilder { + if err := security.ValidateK8sLabel(key, value); err != nil { + logger.Get().Error("Invalid label", "key", key, "value", value, "error", err) + return cb + } + cb.labels[key] = value + return cb +} + +// WithLabels adds multiple label selectors +func (cb *CommandBuilder) WithLabels(labels map[string]string) *CommandBuilder { + for key, value := range labels { + cb.WithLabel(key, value) + } + return cb +} + +// WithAnnotation adds an annotation +func (cb *CommandBuilder) WithAnnotation(key, value string) *CommandBuilder { + if err := security.ValidateK8sLabel(key, value); err != nil { + logger.Get().Error("Invalid annotation", "key", key, "value", value, "error", err) + return cb + } + cb.annotations[key] = value + return cb +} + +// WithTimeout sets the command timeout +func (cb *CommandBuilder) WithTimeout(timeout time.Duration) *CommandBuilder { + cb.timeout = timeout + return cb +} + +// WithDryRun enables dry run mode +func (cb *CommandBuilder) WithDryRun(dryRun bool) *CommandBuilder { + cb.dryRun = dryRun + return cb +} + +// WithForce enables force mode +func (cb *CommandBuilder) WithForce(force bool) *CommandBuilder { + cb.force = force + return cb +} + +// WithWait enables wait mode +func (cb *CommandBuilder) WithWait(wait bool) *CommandBuilder { + cb.wait = wait + return cb +} + +// WithValidation enables/disables validation +func (cb *CommandBuilder) WithValidation(validate bool) *CommandBuilder { + cb.validate = validate + return cb +} + +// WithCache enables caching of the command result +func (cb *CommandBuilder) WithCache(cached bool) *CommandBuilder { + cb.cached = cached + return cb +} + +// WithCacheTTL sets the cache TTL +func (cb *CommandBuilder) WithCacheTTL(ttl time.Duration) *CommandBuilder { + cb.cacheTTL = ttl + return cb +} + +// WithCacheKey sets a custom cache key +func (cb *CommandBuilder) WithCacheKey(key string) *CommandBuilder { + cb.cacheKey = key + return cb +} + +// Build constructs the final command arguments +func (cb *CommandBuilder) Build() (string, []string, error) { + args := make([]string, 0, len(cb.args)+20) + + // Add main arguments + args = append(args, cb.args...) + + // Add namespace if specified + if cb.namespace != "" { + args = append(args, "--namespace", cb.namespace) + } + + // Add context if specified + if cb.context != "" { + args = append(args, "--context", cb.context) + } + + // Add kubeconfig if specified + if cb.kubeconfig != "" { + args = append(args, "--kubeconfig", cb.kubeconfig) + } + + // Add output format + if cb.output != "" { + args = append(args, "--output", cb.output) + } + + // Add label selectors + if len(cb.labels) > 0 { + var labelSelectors []string + for key, value := range cb.labels { + if value != "" { + labelSelectors = append(labelSelectors, fmt.Sprintf("%s=%s", key, value)) + } else { + labelSelectors = append(labelSelectors, key) + } + } + if len(labelSelectors) > 0 { + args = append(args, "--selector", strings.Join(labelSelectors, ",")) + } + } + + // Add timeout only for commands that support it + if cb.timeout > 0 { + if cb.supportsTimeout() { + args = append(args, "--timeout", cb.timeout.String()) + } + } + + // Add dry run + if cb.dryRun { + args = append(args, "--dry-run=client") + } + + // Add force + if cb.force { + args = append(args, "--force") + } + + // Add wait + if cb.wait { + args = append(args, "--wait") + } + + // Add validation + if !cb.validate { + args = append(args, "--validate=false") + } + + return cb.command, args, nil +} + +// supportsTimeout checks if the command supports the --timeout flag +func (cb *CommandBuilder) supportsTimeout() bool { + // For kubectl, many commands support --timeout + if cb.command == "kubectl" { + if len(cb.args) == 0 { + return false + } + + // Check the first argument (subcommand) + subcommand := cb.args[0] + switch subcommand { + case "wait": + return true + case "delete": + // kubectl delete supports --timeout when waiting for deletion + return true + case "rollout": + // kubectl rollout status supports --timeout + if len(cb.args) > 1 && cb.args[1] == "status" { + return true + } + return false + case "apply": + // kubectl apply supports --timeout when used with --wait + return cb.wait + case "annotate", "label": + // kubectl annotate and label support --timeout + return true + case "create": + // kubectl create supports --timeout + return true + case "argo": + // kubectl argo rollouts commands support --timeout + if len(cb.args) > 1 && cb.args[1] == "rollouts" { + return true + } + return false + case "get": + // kubectl get supports --timeout for some operations + return false // Most get operations don't need timeout, they're read-only + default: + return false + } + } + + // For other commands (helm, istioctl, cilium), assume they support timeout + // unless we find specific cases where they don't + return true +} + +// Execute runs the command +func (cb *CommandBuilder) Execute(ctx context.Context) (string, error) { + log := logger.WithContext(ctx) + _, span := telemetry.StartSpan(ctx, "commands.execute", + attribute.String("command", cb.command), + attribute.StringSlice("args", cb.args), + attribute.Bool("cached", cb.cached), + ) + defer span.End() + + command, args, err := cb.Build() + if err != nil { + telemetry.RecordError(span, err, "Command build failed") + log.Error("failed to build command", + "command", cb.command, + "error", err, + ) + return "", err + } + + span.SetAttributes( + attribute.String("built_command", command), + attribute.StringSlice("built_args", args), + ) + + log.Debug("executing command", + "command", command, + "args", args, + "cached", cb.cached, + ) + + // Generate cache key if caching is enabled + if cb.cached { + telemetry.AddEvent(span, "execution.cached") + return cb.executeWithCache(ctx, command, args) + } + + // Execute the command + telemetry.AddEvent(span, "execution.direct") + result, err := cb.executeCommand(ctx, command, args) + if err != nil { + telemetry.RecordError(span, err, "Command execution failed") + return "", err + } + + telemetry.RecordSuccess(span, "Command executed successfully") + span.SetAttributes( + attribute.Int("result_length", len(result)), + ) + + return result, nil +} + +func (cb *CommandBuilder) executeWithCache(ctx context.Context, command string, args []string) (string, error) { + log := logger.WithContext(ctx) + _, span := telemetry.StartSpan(ctx, "commands.executeWithCache", + attribute.String("command", command), + attribute.StringSlice("args", args), + attribute.Bool("cached", true), + ) + defer span.End() + + cacheKey := cb.cacheKey + if cacheKey == "" { + cacheKey = cache.CacheKey(append([]string{command}, args...)...) + } + + log.Info("executing cached command", + "command", command, + "args", args, + "cache_key", cacheKey, + "cache_ttl", cb.cacheTTL.String(), + ) + + // Try to get from cache first + cacheInstance := cache.GetCacheByCommand(command) + + telemetry.AddEvent(span, "cache.lookup", + attribute.String("cache_key", cacheKey), + attribute.String("cache_ttl", cb.cacheTTL.String()), + ) + + result, err := cache.CacheResult(cacheInstance, cacheKey, cb.cacheTTL, func() (string, error) { + telemetry.AddEvent(span, "cache.miss.executing_command") + log.Debug("cache miss, executing command", + "command", command, + "args", args, + ) + return cb.executeCommand(ctx, command, args) + }) + + if err != nil { + telemetry.RecordError(span, err, "Cached command execution failed") + log.Error("cached command execution failed", + "command", command, + "args", args, + "cache_key", cacheKey, + "error", err, + ) + return "", err + } + + telemetry.RecordSuccess(span, "Cached command executed successfully") + log.Info("cached command execution successful", + "command", command, + "args", args, + "cache_key", cacheKey, + "result_length", len(result), + ) + + span.SetAttributes( + attribute.String("cache_key", cacheKey), + attribute.Int("result_length", len(result)), + ) + + return result, nil +} + +// executeCommand executes the actual command +func (cb *CommandBuilder) executeCommand(ctx context.Context, command string, args []string) (string, error) { + executor := cmd.GetShellExecutor(ctx) + output, err := executor.Exec(ctx, command, args...) + if err != nil { + // Create appropriate error based on command type + var toolError *errors.ToolError + switch command { + case "kubectl": + toolError = errors.NewKubernetesError(strings.Join(args, " "), err) + case "helm": + toolError = errors.NewHelmError(strings.Join(args, " "), err) + case "istioctl": + toolError = errors.NewIstioError(strings.Join(args, " "), err) + case "cilium": + toolError = errors.NewCiliumError(strings.Join(args, " "), err) + default: + toolError = errors.NewCommandError(command, err) + } + + return "", toolError + } + + return string(output), nil +} + +// Common command patterns as helper functions + +// GetPods creates a command to get pods +func GetPods(namespace string, labels map[string]string) *CommandBuilder { + builder := KubectlBuilder().WithArgs("get", "pods") + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + if len(labels) > 0 { + builder = builder.WithLabels(labels) + } + + return builder.WithCache(true).WithOutput("json") +} + +// GetServices creates a command to get services +func GetServices(namespace string, labels map[string]string) *CommandBuilder { + builder := KubectlBuilder().WithArgs("get", "services") + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + if len(labels) > 0 { + builder = builder.WithLabels(labels) + } + + return builder.WithCache(true).WithOutput("json") +} + +// GetDeployments creates a command to get deployments +func GetDeployments(namespace string, labels map[string]string) *CommandBuilder { + builder := KubectlBuilder().WithArgs("get", "deployments") + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + if len(labels) > 0 { + builder = builder.WithLabels(labels) + } + + return builder.WithCache(true).WithOutput("json") +} + +// DescribeResource creates a command to describe a resource +func DescribeResource(resourceType, resourceName, namespace string) *CommandBuilder { + builder := KubectlBuilder().WithArgs("describe", resourceType, resourceName) + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + return builder.WithCache(true).WithCacheTTL(2 * time.Minute) +} + +// GetLogs creates a command to get logs +func GetLogs(podName, namespace string, options LogOptions) *CommandBuilder { + builder := KubectlBuilder().WithArgs("logs", podName) + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + if options.Container != "" { + builder = builder.WithArgs("--container", options.Container) + } + + if options.Follow { + builder = builder.WithArgs("--follow") + } + + if options.Previous { + builder = builder.WithArgs("--previous") + } + + if options.Timestamps { + builder = builder.WithArgs("--timestamps") + } + + if options.TailLines > 0 { + builder = builder.WithArgs("--tail", fmt.Sprintf("%d", options.TailLines)) + } + + if options.SinceTime != "" { + builder = builder.WithArgs("--since-time", options.SinceTime) + } + + if options.SinceDuration != "" { + builder = builder.WithArgs("--since", options.SinceDuration) + } + + // Don't cache logs by default as they change frequently + return builder.WithCache(false) +} + +// LogOptions represents options for log commands +type LogOptions struct { + Container string + Follow bool + Previous bool + Timestamps bool + TailLines int + SinceTime string + SinceDuration string +} + +// ApplyResource creates a command to apply a resource +func ApplyResource(filename string, namespace string, options ApplyOptions) *CommandBuilder { + builder := KubectlBuilder().WithArgs("apply", "-f", filename) + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + if options.DryRun { + builder = builder.WithDryRun(true) + } + + if options.Force { + builder = builder.WithForce(true) + } + + if options.Wait { + builder = builder.WithWait(true) + } + + if !options.Validate { + builder = builder.WithValidation(false) + } + + return builder.WithCache(false) // Don't cache apply operations +} + +// ApplyOptions represents options for apply commands +type ApplyOptions struct { + DryRun bool + Force bool + Wait bool + Validate bool +} + +// DeleteResource creates a command to delete a resource +func DeleteResource(resourceType, resourceName, namespace string, options DeleteOptions) *CommandBuilder { + builder := KubectlBuilder().WithArgs("delete", resourceType, resourceName) + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + if options.Force { + builder = builder.WithForce(true) + } + + if options.GracePeriod >= 0 { + builder = builder.WithArgs("--grace-period", fmt.Sprintf("%d", options.GracePeriod)) + } + + if options.Wait { + builder = builder.WithWait(true) + } + + return builder.WithCache(false) // Don't cache delete operations +} + +// DeleteOptions represents options for delete commands +type DeleteOptions struct { + Force bool + GracePeriod int + Wait bool +} + +// HelmInstall creates a command to install a Helm chart +func HelmInstall(releaseName, chart, namespace string, options HelmInstallOptions) *CommandBuilder { + builder := HelmBuilder().WithArgs("install", releaseName, chart) + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + if options.CreateNamespace { + builder = builder.WithArgs("--create-namespace") + } + + if options.DryRun { + builder = builder.WithDryRun(true) + } + + if options.Wait { + builder = builder.WithWait(true) + } + + if options.ValuesFile != "" { + builder = builder.WithArgs("--values", options.ValuesFile) + } + + for key, value := range options.SetValues { + builder = builder.WithArgs("--set", fmt.Sprintf("%s=%s", key, value)) + } + + return builder.WithCache(false) // Don't cache install operations +} + +// HelmInstallOptions represents options for Helm install commands +type HelmInstallOptions struct { + CreateNamespace bool + DryRun bool + Wait bool + ValuesFile string + SetValues map[string]string +} + +// HelmList creates a command to list Helm releases +func HelmList(namespace string, options HelmListOptions) *CommandBuilder { + builder := HelmBuilder().WithArgs("list") + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + if options.AllNamespaces { + builder = builder.WithArgs("--all-namespaces") + } + + if options.Output != "" { + builder = builder.WithOutput(options.Output) + } + + return builder.WithCache(true).WithCacheTTL(2 * time.Minute) +} + +// HelmListOptions represents options for Helm list commands +type HelmListOptions struct { + AllNamespaces bool + Output string +} + +// IstioProxyStatus creates a command to get Istio proxy status +func IstioProxyStatus(podName, namespace string) *CommandBuilder { + builder := IstioCtlBuilder().WithArgs("proxy-status") + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + if podName != "" { + builder = builder.WithArgs(podName) + } + + return builder.WithCache(true).WithCacheTTL(30 * time.Second) +} + +// CiliumStatus creates a command to get Cilium status +func CiliumStatus() *CommandBuilder { + return CiliumBuilder().WithArgs("status").WithCache(true).WithCacheTTL(30 * time.Second) +} + +// ArgoRolloutsGet creates a command to get Argo rollouts +func ArgoRolloutsGet(rolloutName, namespace string) *CommandBuilder { + builder := ArgoRolloutsBuilder().WithArgs("get", "rollout") + + if rolloutName != "" { + builder = builder.WithArgs(rolloutName) + } + + if namespace != "" { + builder = builder.WithNamespace(namespace) + } + + return builder.WithCache(true).WithCacheTTL(1 * time.Minute) +} diff --git a/internal/commands/builder_test.go b/internal/commands/builder_test.go new file mode 100644 index 00000000..e326a763 --- /dev/null +++ b/internal/commands/builder_test.go @@ -0,0 +1,585 @@ +package commands + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCommandBuilder(t *testing.T) { + cb := NewCommandBuilder("test-command") + + assert.Equal(t, "test-command", cb.command) + assert.Empty(t, cb.args) + assert.Empty(t, cb.namespace) + assert.Empty(t, cb.context) + assert.Empty(t, cb.kubeconfig) + assert.Empty(t, cb.output) + assert.NotNil(t, cb.labels) + assert.NotNil(t, cb.annotations) + assert.Equal(t, 30*time.Second, cb.timeout) + assert.Equal(t, 5*time.Minute, cb.cacheTTL) + assert.True(t, cb.validate) + assert.False(t, cb.cached) + assert.False(t, cb.dryRun) + assert.False(t, cb.force) + assert.False(t, cb.wait) +} + +func TestCommandBuilderFactories(t *testing.T) { + tests := []struct { + name string + factory func() *CommandBuilder + expected string + }{ + {"kubectl", KubectlBuilder, "kubectl"}, + {"helm", HelmBuilder, "helm"}, + {"istioctl", IstioCtlBuilder, "istioctl"}, + {"cilium", CiliumBuilder, "cilium"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cb := tt.factory() + assert.Equal(t, tt.expected, cb.command) + }) + } +} + +func TestArgoRolloutsBuilder(t *testing.T) { + cb := ArgoRolloutsBuilder() + + assert.Equal(t, "kubectl", cb.command) + assert.Equal(t, []string{"argo", "rollouts"}, cb.args) +} + +func TestCommandBuilderWithArgs(t *testing.T) { + cb := NewCommandBuilder("test").WithArgs("arg1", "arg2") + + assert.Equal(t, []string{"arg1", "arg2"}, cb.args) + + // Test chaining + cb.WithArgs("arg3") + assert.Equal(t, []string{"arg1", "arg2", "arg3"}, cb.args) +} + +func TestCommandBuilderWithNamespace(t *testing.T) { + cb := NewCommandBuilder("test").WithNamespace("default") + + assert.Equal(t, "default", cb.namespace) + + // Test invalid namespace - should not set the namespace + cb.WithNamespace("invalid..namespace") + assert.Equal(t, "default", cb.namespace) // Should remain unchanged +} + +func TestCommandBuilderWithContext(t *testing.T) { + cb := NewCommandBuilder("test").WithContext("minikube") + + assert.Equal(t, "minikube", cb.context) +} + +func TestCommandBuilderWithKubeconfig(t *testing.T) { + cb := NewCommandBuilder("test").WithKubeconfig("/path/to/config") + + assert.Equal(t, "/path/to/config", cb.kubeconfig) +} + +func TestCommandBuilderWithOutput(t *testing.T) { + validOutputs := []string{"json", "yaml", "wide", "name"} + + for _, output := range validOutputs { + cb := NewCommandBuilder("test").WithOutput(output) + assert.Equal(t, output, cb.output) + } + + // Test invalid output + cb := NewCommandBuilder("test").WithOutput("invalid") + assert.Empty(t, cb.output) +} + +func TestCommandBuilderWithLabel(t *testing.T) { + cb := NewCommandBuilder("test").WithLabel("app", "web") + + assert.Equal(t, "web", cb.labels["app"]) +} + +func TestCommandBuilderWithLabels(t *testing.T) { + labels := map[string]string{ + "app": "web", + "version": "v1.0.0", + } + + cb := NewCommandBuilder("test").WithLabels(labels) + + assert.Equal(t, labels["app"], cb.labels["app"]) + assert.Equal(t, labels["version"], cb.labels["version"]) +} + +func TestCommandBuilderWithAnnotation(t *testing.T) { + cb := NewCommandBuilder("test").WithAnnotation("simple-key", "value") + + // The annotation should be accepted if it's a valid format + assert.Equal(t, "value", cb.annotations["simple-key"]) + + // Test with invalid annotation - still gets added but logs an error + cb2 := NewCommandBuilder("test").WithAnnotation("invalid..key", "value") + assert.Equal(t, "value", cb2.annotations["invalid..key"]) // Invalid annotations are still added but logged +} + +func TestCommandBuilderWithTimeout(t *testing.T) { + timeout := 60 * time.Second + cb := NewCommandBuilder("test").WithTimeout(timeout) + + assert.Equal(t, timeout, cb.timeout) +} + +func TestCommandBuilderWithFlags(t *testing.T) { + cb := NewCommandBuilder("test"). + WithDryRun(true). + WithForce(true). + WithWait(true). + WithValidation(false) + + assert.True(t, cb.dryRun) + assert.True(t, cb.force) + assert.True(t, cb.wait) + assert.False(t, cb.validate) +} + +func TestCommandBuilderWithCache(t *testing.T) { + cb := NewCommandBuilder("test").WithCache(true) + + assert.True(t, cb.cached) +} + +func TestCommandBuilderWithCacheTTL(t *testing.T) { + ttl := 10 * time.Minute + cb := NewCommandBuilder("test").WithCacheTTL(ttl) + + assert.Equal(t, ttl, cb.cacheTTL) +} + +func TestCommandBuilderWithCacheKey(t *testing.T) { + cb := NewCommandBuilder("test").WithCacheKey("custom-key") + + assert.Equal(t, "custom-key", cb.cacheKey) +} + +func TestCommandBuilderBuild(t *testing.T) { + cb := NewCommandBuilder("kubectl"). + WithArgs("get", "pods"). + WithNamespace("default"). + WithContext("minikube"). + WithKubeconfig("/path/to/config"). + WithOutput("json"). + WithLabel("app", "web"). + WithDryRun(true). + WithForce(true). + WithWait(true). + WithValidation(false) + + command, args, err := cb.Build() + require.NoError(t, err) + + assert.Equal(t, "kubectl", command) + assert.Contains(t, args, "get") + assert.Contains(t, args, "pods") + assert.Contains(t, args, "--namespace") + assert.Contains(t, args, "default") + assert.Contains(t, args, "--context") + assert.Contains(t, args, "minikube") + assert.Contains(t, args, "--kubeconfig") + assert.Contains(t, args, "/path/to/config") + assert.Contains(t, args, "--output") + assert.Contains(t, args, "json") + assert.Contains(t, args, "--selector") + assert.Contains(t, args, "app=web") + assert.Contains(t, args, "--dry-run=client") + assert.Contains(t, args, "--force") + assert.Contains(t, args, "--wait") + assert.Contains(t, args, "--validate=false") +} + +func TestCommandBuilderBuildWithTimeout(t *testing.T) { + cb := NewCommandBuilder("kubectl"). + WithArgs("delete", "pod", "test-pod"). + WithTimeout(45 * time.Second) + + command, args, err := cb.Build() + require.NoError(t, err) + + assert.Equal(t, "kubectl", command) + assert.Contains(t, args, "--timeout") + assert.Contains(t, args, "45s") +} + +func TestCommandBuilderBuildWithMultipleLabels(t *testing.T) { + cb := NewCommandBuilder("kubectl"). + WithArgs("get", "pods"). + WithLabel("app", "web"). + WithLabel("version", "v1.0.0") + + command, args, err := cb.Build() + require.NoError(t, err) + + assert.Equal(t, "kubectl", command) + assert.Contains(t, args, "--selector") + + // Find the selector argument + var selectorValue string + for i, arg := range args { + if arg == "--selector" && i+1 < len(args) { + selectorValue = args[i+1] + break + } + } + + assert.Contains(t, selectorValue, "app=web") + assert.Contains(t, selectorValue, "version=v1.0.0") +} + +func TestGetPods(t *testing.T) { + namespace := "default" + labels := map[string]string{"app": "web"} + + cb := GetPods(namespace, labels) + + assert.Equal(t, "kubectl", cb.command) + assert.Contains(t, cb.args, "get") + assert.Contains(t, cb.args, "pods") + assert.Equal(t, namespace, cb.namespace) + assert.Equal(t, labels, cb.labels) + assert.True(t, cb.cached) + assert.Equal(t, "json", cb.output) +} + +func TestGetServices(t *testing.T) { + namespace := "default" + labels := map[string]string{"app": "web"} + + cb := GetServices(namespace, labels) + + assert.Equal(t, "kubectl", cb.command) + assert.Contains(t, cb.args, "get") + assert.Contains(t, cb.args, "services") + assert.Equal(t, namespace, cb.namespace) + assert.Equal(t, labels, cb.labels) + assert.True(t, cb.cached) + assert.Equal(t, "json", cb.output) +} + +func TestGetDeployments(t *testing.T) { + namespace := "default" + labels := map[string]string{"app": "web"} + + cb := GetDeployments(namespace, labels) + + assert.Equal(t, "kubectl", cb.command) + assert.Contains(t, cb.args, "get") + assert.Contains(t, cb.args, "deployments") + assert.Equal(t, namespace, cb.namespace) + assert.Equal(t, labels, cb.labels) + assert.True(t, cb.cached) + assert.Equal(t, "json", cb.output) +} + +func TestDescribeResource(t *testing.T) { + resourceType := "pod" + resourceName := "test-pod" + namespace := "default" + + cb := DescribeResource(resourceType, resourceName, namespace) + + assert.Equal(t, "kubectl", cb.command) + assert.Contains(t, cb.args, "describe") + assert.Contains(t, cb.args, resourceType) + assert.Contains(t, cb.args, resourceName) + assert.Equal(t, namespace, cb.namespace) + assert.True(t, cb.cached) + assert.Equal(t, 2*time.Minute, cb.cacheTTL) +} + +func TestGetLogs(t *testing.T) { + podName := "test-pod" + namespace := "default" + options := LogOptions{ + Container: "app", + Follow: true, + Previous: false, + Timestamps: true, + TailLines: 100, + SinceTime: "2023-01-01T00:00:00Z", + SinceDuration: "1h", + } + + cb := GetLogs(podName, namespace, options) + + assert.Equal(t, "kubectl", cb.command) + assert.Contains(t, cb.args, "logs") + assert.Contains(t, cb.args, podName) + assert.Equal(t, namespace, cb.namespace) + assert.Contains(t, cb.args, "--container") + assert.Contains(t, cb.args, "app") + assert.Contains(t, cb.args, "--follow") + assert.Contains(t, cb.args, "--timestamps") + assert.Contains(t, cb.args, "--tail") + assert.Contains(t, cb.args, "100") + assert.Contains(t, cb.args, "--since-time") + assert.Contains(t, cb.args, "2023-01-01T00:00:00Z") + assert.Contains(t, cb.args, "--since") + assert.Contains(t, cb.args, "1h") + assert.False(t, cb.cached) +} + +func TestGetLogsWithPrevious(t *testing.T) { + podName := "test-pod" + namespace := "default" + options := LogOptions{ + Previous: true, + } + + cb := GetLogs(podName, namespace, options) + + assert.Contains(t, cb.args, "--previous") +} + +func TestApplyResource(t *testing.T) { + filename := "/path/to/resource.yaml" + namespace := "default" + options := ApplyOptions{ + DryRun: true, + Force: true, + Wait: true, + Validate: false, + } + + cb := ApplyResource(filename, namespace, options) + + assert.Equal(t, "kubectl", cb.command) + assert.Contains(t, cb.args, "apply") + assert.Contains(t, cb.args, "-f") + assert.Contains(t, cb.args, filename) + assert.Equal(t, namespace, cb.namespace) + assert.True(t, cb.dryRun) + assert.True(t, cb.force) + assert.True(t, cb.wait) + assert.False(t, cb.validate) + assert.False(t, cb.cached) +} + +func TestDeleteResource(t *testing.T) { + resourceType := "pod" + resourceName := "test-pod" + namespace := "default" + options := DeleteOptions{ + Force: true, + GracePeriod: 30, + Wait: true, + } + + cb := DeleteResource(resourceType, resourceName, namespace, options) + + assert.Equal(t, "kubectl", cb.command) + assert.Contains(t, cb.args, "delete") + assert.Contains(t, cb.args, resourceType) + assert.Contains(t, cb.args, resourceName) + assert.Equal(t, namespace, cb.namespace) + assert.True(t, cb.force) + assert.True(t, cb.wait) + assert.False(t, cb.cached) +} + +func TestHelmInstall(t *testing.T) { + releaseName := "test-release" + chart := "bitnami/nginx" + namespace := "default" + options := HelmInstallOptions{ + CreateNamespace: true, + DryRun: true, + Wait: true, + ValuesFile: "/path/to/values.yaml", + SetValues: map[string]string{"image.tag": "1.20"}, + } + + cb := HelmInstall(releaseName, chart, namespace, options) + + assert.Equal(t, "helm", cb.command) + assert.Contains(t, cb.args, "install") + assert.Contains(t, cb.args, releaseName) + assert.Contains(t, cb.args, chart) + assert.Equal(t, namespace, cb.namespace) + assert.True(t, cb.dryRun) + assert.True(t, cb.wait) + assert.False(t, cb.cached) +} + +func TestHelmList(t *testing.T) { + namespace := "default" + options := HelmListOptions{ + AllNamespaces: true, + Output: "json", + } + + cb := HelmList(namespace, options) + + assert.Equal(t, "helm", cb.command) + assert.Contains(t, cb.args, "list") + assert.Equal(t, namespace, cb.namespace) + assert.Equal(t, "json", cb.output) + assert.True(t, cb.cached) +} + +func TestIstioProxyStatus(t *testing.T) { + podName := "test-pod" + namespace := "default" + + cb := IstioProxyStatus(podName, namespace) + + assert.Equal(t, "istioctl", cb.command) + assert.Contains(t, cb.args, "proxy-status") + assert.Contains(t, cb.args, podName) + assert.Equal(t, namespace, cb.namespace) + assert.True(t, cb.cached) +} + +func TestCiliumStatus(t *testing.T) { + cb := CiliumStatus() + + assert.Equal(t, "cilium", cb.command) + assert.Contains(t, cb.args, "status") + assert.Empty(t, cb.output) // CiliumStatus doesn't set output format + assert.True(t, cb.cached) +} + +func TestArgoRolloutsGet(t *testing.T) { + rolloutName := "test-rollout" + namespace := "default" + + cb := ArgoRolloutsGet(rolloutName, namespace) + + assert.Equal(t, "kubectl", cb.command) + assert.Contains(t, cb.args, "argo") + assert.Contains(t, cb.args, "rollouts") + assert.Contains(t, cb.args, "get") + assert.Contains(t, cb.args, "rollout") + assert.Contains(t, cb.args, rolloutName) + assert.Equal(t, namespace, cb.namespace) + assert.Empty(t, cb.output) // ArgoRolloutsGet doesn't set output format + assert.True(t, cb.cached) +} + +func TestCommandBuilderChaining(t *testing.T) { + cb := NewCommandBuilder("kubectl"). + WithArgs("get", "pods"). + WithNamespace("default"). + WithOutput("json"). + WithLabel("app", "web"). + WithTimeout(60 * time.Second). + WithCache(true). + WithCacheTTL(10 * time.Minute) + + assert.Equal(t, "kubectl", cb.command) + assert.Equal(t, []string{"get", "pods"}, cb.args) + assert.Equal(t, "default", cb.namespace) + assert.Equal(t, "json", cb.output) + assert.Equal(t, "web", cb.labels["app"]) + assert.Equal(t, 60*time.Second, cb.timeout) + assert.True(t, cb.cached) + assert.Equal(t, 10*time.Minute, cb.cacheTTL) +} + +func TestCommandBuilderEmptyNamespace(t *testing.T) { + cb := GetPods("", nil) + + assert.Empty(t, cb.namespace) +} + +func TestCommandBuilderEmptyLabels(t *testing.T) { + cb := GetPods("default", nil) + + assert.Empty(t, cb.labels) +} + +func TestLogOptionsDefaults(t *testing.T) { + options := LogOptions{} + + assert.False(t, options.Follow) + assert.False(t, options.Previous) + assert.False(t, options.Timestamps) + assert.Equal(t, 0, options.TailLines) + assert.Empty(t, options.SinceTime) + assert.Empty(t, options.SinceDuration) +} + +func TestApplyOptionsDefaults(t *testing.T) { + options := ApplyOptions{} + + assert.False(t, options.DryRun) + assert.False(t, options.Force) + assert.False(t, options.Wait) + assert.False(t, options.Validate) +} + +func TestDeleteOptionsDefaults(t *testing.T) { + options := DeleteOptions{} + + assert.False(t, options.Force) + assert.Equal(t, 0, options.GracePeriod) + assert.False(t, options.Wait) +} + +func TestHelmInstallOptionsDefaults(t *testing.T) { + options := HelmInstallOptions{} + + assert.False(t, options.CreateNamespace) + assert.False(t, options.DryRun) + assert.False(t, options.Wait) + assert.Empty(t, options.ValuesFile) + assert.Nil(t, options.SetValues) +} + +func TestHelmListOptionsDefaults(t *testing.T) { + options := HelmListOptions{} + + assert.False(t, options.AllNamespaces) + assert.Empty(t, options.Output) +} + +// Mock tests for Execute method - these would need a mock for utils.RunCommandWithContext +func TestCommandBuilderExecuteWithoutCache(t *testing.T) { + cb := NewCommandBuilder("echo"). + WithArgs("hello", "world"). + WithCache(false) + + // This test would need mocking to work properly + // For now, we'll just verify the command building part + command, args, err := cb.Build() + require.NoError(t, err) + + assert.Equal(t, "echo", command) + assert.Contains(t, args, "hello") + assert.Contains(t, args, "world") + assert.Contains(t, args, "--timeout") + assert.Contains(t, args, "30s") +} + +func TestCommandBuilderExecuteWithCache(t *testing.T) { + cb := NewCommandBuilder("echo"). + WithArgs("hello", "world"). + WithCache(true) + + // This test would need mocking to work properly + // For now, we'll just verify the command building part + command, args, err := cb.Build() + require.NoError(t, err) + + assert.Equal(t, "echo", command) + assert.Contains(t, args, "hello") + assert.Contains(t, args, "world") + assert.Contains(t, args, "--timeout") + assert.Contains(t, args, "30s") + assert.True(t, cb.cached) +} diff --git a/internal/errors/tool_errors.go b/internal/errors/tool_errors.go new file mode 100644 index 00000000..26771647 --- /dev/null +++ b/internal/errors/tool_errors.go @@ -0,0 +1,352 @@ +package errors + +import ( + "fmt" + "strings" + "time" + + "github.com/mark3labs/mcp-go/mcp" +) + +// ToolError represents a structured error with context and recovery suggestions +type ToolError struct { + Operation string `json:"operation"` + Cause error `json:"cause"` + Suggestions []string `json:"suggestions"` + IsRetryable bool `json:"is_retryable"` + Timestamp time.Time `json:"timestamp"` + ErrorCode string `json:"error_code"` + Component string `json:"component"` + ResourceType string `json:"resource_type,omitempty"` + ResourceName string `json:"resource_name,omitempty"` + Context map[string]interface{} `json:"context,omitempty"` +} + +// Error implements the error interface +func (e *ToolError) Error() string { + return fmt.Sprintf("[%s] %s failed: %v", e.Component, e.Operation, e.Cause) +} + +// ToMCPResult converts the error to an MCP result with rich context +func (e *ToolError) ToMCPResult() *mcp.CallToolResult { + var message strings.Builder + + // Format the error message with context + message.WriteString(fmt.Sprintf("❌ **%s Error**\n\n", e.Component)) + message.WriteString(fmt.Sprintf("**Operation**: %s\n", e.Operation)) + message.WriteString(fmt.Sprintf("**Error**: %s\n", e.Cause.Error())) + + if e.ResourceType != "" { + message.WriteString(fmt.Sprintf("**Resource Type**: %s\n", e.ResourceType)) + } + + if e.ResourceName != "" { + message.WriteString(fmt.Sprintf("**Resource Name**: %s\n", e.ResourceName)) + } + + message.WriteString(fmt.Sprintf("**Error Code**: %s\n", e.ErrorCode)) + message.WriteString(fmt.Sprintf("**Timestamp**: %s\n", e.Timestamp.Format(time.RFC3339))) + + if e.IsRetryable { + message.WriteString("**Retryable**: Yes\n") + } else { + message.WriteString("**Retryable**: No\n") + } + + if len(e.Suggestions) > 0 { + message.WriteString("\n**💡 Suggestions**:\n") + for i, suggestion := range e.Suggestions { + message.WriteString(fmt.Sprintf("%d. %s\n", i+1, suggestion)) + } + } + + if len(e.Context) > 0 { + message.WriteString("\n**📋 Context**:\n") + for key, value := range e.Context { + message.WriteString(fmt.Sprintf("- %s: %v\n", key, value)) + } + } + + return mcp.NewToolResultError(message.String()) +} + +// NewToolError creates a new structured tool error +func NewToolError(component, operation string, cause error) *ToolError { + return &ToolError{ + Operation: operation, + Cause: cause, + Suggestions: []string{}, + IsRetryable: false, + Timestamp: time.Now(), + ErrorCode: "UNKNOWN", + Component: component, + Context: make(map[string]interface{}), + } +} + +// WithSuggestions adds recovery suggestions to the error +func (e *ToolError) WithSuggestions(suggestions ...string) *ToolError { + e.Suggestions = append(e.Suggestions, suggestions...) + return e +} + +// WithRetryable sets whether the error is retryable +func (e *ToolError) WithRetryable(retryable bool) *ToolError { + e.IsRetryable = retryable + return e +} + +// WithErrorCode sets the error code +func (e *ToolError) WithErrorCode(code string) *ToolError { + e.ErrorCode = code + return e +} + +// WithResource adds resource information to the error +func (e *ToolError) WithResource(resourceType, resourceName string) *ToolError { + e.ResourceType = resourceType + e.ResourceName = resourceName + return e +} + +// WithContext adds contextual information to the error +func (e *ToolError) WithContext(key string, value interface{}) *ToolError { + e.Context[key] = value + return e +} + +// Common error creators for different components + +// NewKubernetesError creates a Kubernetes-specific error +func NewKubernetesError(operation string, cause error) *ToolError { + err := NewToolError("Kubernetes", operation, cause) + + // Add Kubernetes-specific suggestions based on common errors + if strings.Contains(cause.Error(), "connection refused") { + err = err.WithSuggestions( + "Check if the Kubernetes cluster is running", + "Verify your kubeconfig is correct", + "Ensure network connectivity to the cluster", + ).WithRetryable(true).WithErrorCode("K8S_CONNECTION_ERROR") + } else if strings.Contains(cause.Error(), "forbidden") { + err = err.WithSuggestions( + "Check your RBAC permissions", + "Verify your service account has the required permissions", + "Contact your cluster administrator", + ).WithRetryable(false).WithErrorCode("K8S_PERMISSION_ERROR") + } else if strings.Contains(cause.Error(), "not found") { + err = err.WithSuggestions( + "Check if the resource exists", + "Verify the resource name and namespace", + "List available resources to confirm", + ).WithRetryable(false).WithErrorCode("K8S_RESOURCE_NOT_FOUND") + } else if strings.Contains(cause.Error(), "already exists") { + err = err.WithSuggestions( + "Use a different name for the resource", + "Delete the existing resource first", + "Use 'kubectl apply' instead of 'kubectl create'", + ).WithRetryable(false).WithErrorCode("K8S_RESOURCE_EXISTS") + } else { + err = err.WithSuggestions( + "Check the kubectl command syntax", + "Verify your kubeconfig is valid", + "Check cluster connectivity", + ).WithRetryable(true).WithErrorCode("K8S_GENERIC_ERROR") + } + + return err +} + +// NewHelmError creates a Helm-specific error +func NewHelmError(operation string, cause error) *ToolError { + err := NewToolError("Helm", operation, cause) + + if strings.Contains(cause.Error(), "not found") { + err = err.WithSuggestions( + "Check if the Helm release exists", + "Verify the release name and namespace", + "Use 'helm list' to see available releases", + ).WithRetryable(false).WithErrorCode("HELM_RELEASE_NOT_FOUND") + } else if strings.Contains(cause.Error(), "already exists") { + err = err.WithSuggestions( + "Use a different release name", + "Upgrade the existing release instead", + "Uninstall the existing release first", + ).WithRetryable(false).WithErrorCode("HELM_RELEASE_EXISTS") + } else if strings.Contains(cause.Error(), "repository") { + err = err.WithSuggestions( + "Add the required Helm repository", + "Update your Helm repositories", + "Check repository URL and credentials", + ).WithRetryable(true).WithErrorCode("HELM_REPOSITORY_ERROR") + } else { + err = err.WithSuggestions( + "Check the Helm command syntax", + "Verify your kubeconfig is valid", + "Ensure Helm is properly installed", + ).WithRetryable(true).WithErrorCode("HELM_GENERIC_ERROR") + } + + return err +} + +// NewIstioError creates an Istio-specific error +func NewIstioError(operation string, cause error) *ToolError { + err := NewToolError("Istio", operation, cause) + + if strings.Contains(cause.Error(), "not found") { + err = err.WithSuggestions( + "Check if Istio is installed in the cluster", + "Verify the pod/service name and namespace", + "Ensure Istio sidecar is injected", + ).WithRetryable(false).WithErrorCode("ISTIO_RESOURCE_NOT_FOUND") + } else if strings.Contains(cause.Error(), "connection refused") { + err = err.WithSuggestions( + "Check if Istio control plane is running", + "Verify Istio proxy is healthy", + "Check network policies", + ).WithRetryable(true).WithErrorCode("ISTIO_CONNECTION_ERROR") + } else { + err = err.WithSuggestions( + "Check istioctl command syntax", + "Verify Istio installation", + "Check Istio proxy status", + ).WithRetryable(true).WithErrorCode("ISTIO_GENERIC_ERROR") + } + + return err +} + +// NewPrometheusError creates a Prometheus-specific error +func NewPrometheusError(operation string, cause error) *ToolError { + err := NewToolError("Prometheus", operation, cause) + + if strings.Contains(cause.Error(), "connection refused") { + err = err.WithSuggestions( + "Check if Prometheus server is running", + "Verify the Prometheus URL", + "Check network connectivity", + ).WithRetryable(true).WithErrorCode("PROMETHEUS_CONNECTION_ERROR") + } else if strings.Contains(cause.Error(), "parse error") { + err = err.WithSuggestions( + "Check your PromQL query syntax", + "Verify metric names and labels", + "Test the query in Prometheus UI", + ).WithRetryable(false).WithErrorCode("PROMETHEUS_QUERY_ERROR") + } else { + err = err.WithSuggestions( + "Check Prometheus server status", + "Verify the query format", + "Check authentication if required", + ).WithRetryable(true).WithErrorCode("PROMETHEUS_GENERIC_ERROR") + } + + return err +} + +// NewArgoError creates an Argo-specific error +func NewArgoError(operation string, cause error) *ToolError { + err := NewToolError("Argo Rollouts", operation, cause) + + if strings.Contains(cause.Error(), "not found") { + err = err.WithSuggestions( + "Check if Argo Rollouts is installed", + "Verify the rollout name and namespace", + "Use 'kubectl get rollouts' to list available rollouts", + ).WithRetryable(false).WithErrorCode("ARGO_ROLLOUT_NOT_FOUND") + } else if strings.Contains(cause.Error(), "plugin") { + err = err.WithSuggestions( + "Install the kubectl argo rollouts plugin", + "Check plugin version compatibility", + "Verify plugin installation path", + ).WithRetryable(true).WithErrorCode("ARGO_PLUGIN_ERROR") + } else { + err = err.WithSuggestions( + "Check Argo Rollouts installation", + "Verify the command syntax", + "Check RBAC permissions", + ).WithRetryable(true).WithErrorCode("ARGO_GENERIC_ERROR") + } + + return err +} + +// NewCiliumError creates a Cilium-specific error +func NewCiliumError(operation string, cause error) *ToolError { + err := NewToolError("Cilium", operation, cause) + + if strings.Contains(cause.Error(), "not found") { + err = err.WithSuggestions( + "Check if Cilium is installed", + "Verify the cilium CLI is installed", + "Check Cilium agent status", + ).WithRetryable(false).WithErrorCode("CILIUM_NOT_FOUND") + } else if strings.Contains(cause.Error(), "connection") { + err = err.WithSuggestions( + "Check Cilium agent connectivity", + "Verify cluster mesh configuration", + "Check Cilium operator status", + ).WithRetryable(true).WithErrorCode("CILIUM_CONNECTION_ERROR") + } else { + err = err.WithSuggestions( + "Check Cilium installation", + "Verify cilium CLI version", + "Check Cilium system pods", + ).WithRetryable(true).WithErrorCode("CILIUM_GENERIC_ERROR") + } + + return err +} + +// NewValidationError creates a validation error +func NewValidationError(field, message string) *ToolError { + err := NewToolError("Validation", fmt.Sprintf("validate %s", field), fmt.Errorf("%s", message)) + + err = err.WithSuggestions( + "Check the input format", + "Refer to the documentation for valid values", + "Verify the parameter requirements", + ).WithRetryable(false).WithErrorCode("VALIDATION_ERROR") + + return err +} + +// NewSecurityError creates a security-related error +func NewSecurityError(operation string, cause error) *ToolError { + err := NewToolError("Security", operation, cause) + + err = err.WithSuggestions( + "Review the input for potentially dangerous content", + "Use only trusted input sources", + "Contact security team if needed", + ).WithRetryable(false).WithErrorCode("SECURITY_ERROR") + + return err +} + +// NewTimeoutError creates a timeout error +func NewTimeoutError(operation string, timeout time.Duration) *ToolError { + cause := fmt.Errorf("operation timed out after %v", timeout) + err := NewToolError("Timeout", operation, cause) + + err = err.WithSuggestions( + "Try the operation again", + "Check network connectivity", + "Increase timeout if possible", + ).WithRetryable(true).WithErrorCode("TIMEOUT_ERROR") + + return err +} + +// NewCommandError creates a command execution error +func NewCommandError(command string, cause error) *ToolError { + err := NewToolError("Command", fmt.Sprintf("execute %s", command), cause) + + err = err.WithSuggestions( + "Check if the command exists in PATH", + "Verify command syntax and arguments", + "Check system permissions", + ).WithRetryable(true).WithErrorCode("COMMAND_ERROR") + + return err +} diff --git a/internal/errors/tool_errors_test.go b/internal/errors/tool_errors_test.go new file mode 100644 index 00000000..bfa2f24c --- /dev/null +++ b/internal/errors/tool_errors_test.go @@ -0,0 +1,366 @@ +package errors + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNewToolError(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause) + + assert.Equal(t, "test operation", err.Operation) + assert.Equal(t, cause, err.Cause) + assert.Equal(t, "TestComponent", err.Component) + assert.Equal(t, "UNKNOWN", err.ErrorCode) + assert.False(t, err.IsRetryable) + assert.Empty(t, err.Suggestions) + assert.NotNil(t, err.Context) + assert.WithinDuration(t, time.Now(), err.Timestamp, time.Second) +} + +func TestToolErrorError(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause) + + result := err.Error() + expected := "[TestComponent] test operation failed: test error" + assert.Equal(t, expected, result) +} + +func TestToolErrorWithSuggestions(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause) + + err = err.WithSuggestions("suggestion 1", "suggestion 2") + + assert.Equal(t, []string{"suggestion 1", "suggestion 2"}, err.Suggestions) + + // Test chaining + err = err.WithSuggestions("suggestion 3") + assert.Equal(t, []string{"suggestion 1", "suggestion 2", "suggestion 3"}, err.Suggestions) +} + +func TestToolErrorWithRetryable(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause) + + err = err.WithRetryable(true) + assert.True(t, err.IsRetryable) + + err = err.WithRetryable(false) + assert.False(t, err.IsRetryable) +} + +func TestToolErrorWithErrorCode(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause) + + err = err.WithErrorCode("TEST_ERROR") + assert.Equal(t, "TEST_ERROR", err.ErrorCode) +} + +func TestToolErrorWithResource(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause) + + err = err.WithResource("Pod", "test-pod") + assert.Equal(t, "Pod", err.ResourceType) + assert.Equal(t, "test-pod", err.ResourceName) +} + +func TestToolErrorWithContext(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause) + + err = err.WithContext("key1", "value1") + err = err.WithContext("key2", 42) + + assert.Equal(t, "value1", err.Context["key1"]) + assert.Equal(t, 42, err.Context["key2"]) +} + +func TestToolErrorToMCPResult(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause). + WithErrorCode("TEST_ERROR"). + WithResource("Pod", "test-pod"). + WithSuggestions("suggestion 1", "suggestion 2"). + WithContext("key1", "value1"). + WithRetryable(true) + + result := err.ToMCPResult() + + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.NotEmpty(t, result.Content) + + // Check content (assuming it's text content) + if len(result.Content) > 0 { + content := result.Content[0] + // This depends on the actual MCP implementation + // We'll just check that it's not empty + assert.NotNil(t, content) + } +} + +func TestNewKubernetesError(t *testing.T) { + tests := []struct { + name string + causeError string + expectedCode string + expectedRetry bool + expectedSuggs int + }{ + { + name: "connection refused", + causeError: "connection refused", + expectedCode: "K8S_CONNECTION_ERROR", + expectedRetry: true, + expectedSuggs: 3, + }, + { + name: "forbidden", + causeError: "forbidden", + expectedCode: "K8S_PERMISSION_ERROR", + expectedRetry: false, + expectedSuggs: 3, + }, + { + name: "not found", + causeError: "not found", + expectedCode: "K8S_RESOURCE_NOT_FOUND", + expectedRetry: false, + expectedSuggs: 3, + }, + { + name: "already exists", + causeError: "already exists", + expectedCode: "K8S_RESOURCE_EXISTS", + expectedRetry: false, + expectedSuggs: 3, + }, + { + name: "generic error", + causeError: "some other error", + expectedCode: "K8S_GENERIC_ERROR", + expectedRetry: true, + expectedSuggs: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cause := errors.New(tt.causeError) + err := NewKubernetesError("test operation", cause) + + assert.Equal(t, "Kubernetes", err.Component) + assert.Equal(t, tt.expectedCode, err.ErrorCode) + assert.Equal(t, tt.expectedRetry, err.IsRetryable) + assert.Len(t, err.Suggestions, tt.expectedSuggs) + }) + } +} + +func TestNewHelmError(t *testing.T) { + tests := []struct { + name string + causeError string + expectedCode string + expectedRetry bool + expectedSuggs int + }{ + { + name: "not found", + causeError: "not found", + expectedCode: "HELM_RELEASE_NOT_FOUND", + expectedRetry: false, + expectedSuggs: 3, + }, + { + name: "already exists", + causeError: "already exists", + expectedCode: "HELM_RELEASE_EXISTS", + expectedRetry: false, + expectedSuggs: 3, + }, + { + name: "repository error", + causeError: "repository error", + expectedCode: "HELM_REPOSITORY_ERROR", + expectedRetry: true, + expectedSuggs: 3, + }, + { + name: "generic error", + causeError: "some other error", + expectedCode: "HELM_GENERIC_ERROR", + expectedRetry: true, + expectedSuggs: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cause := errors.New(tt.causeError) + err := NewHelmError("test operation", cause) + + assert.Equal(t, "Helm", err.Component) + assert.Equal(t, tt.expectedCode, err.ErrorCode) + assert.Equal(t, tt.expectedRetry, err.IsRetryable) + assert.Len(t, err.Suggestions, tt.expectedSuggs) + }) + } +} + +func TestNewIstioError(t *testing.T) { + cause := errors.New("test error") + err := NewIstioError("test operation", cause) + + assert.Equal(t, "Istio", err.Component) + assert.Equal(t, "test operation", err.Operation) + assert.Equal(t, cause, err.Cause) +} + +func TestNewPrometheusError(t *testing.T) { + cause := errors.New("test error") + err := NewPrometheusError("test operation", cause) + + assert.Equal(t, "Prometheus", err.Component) + assert.Equal(t, "test operation", err.Operation) + assert.Equal(t, cause, err.Cause) +} + +func TestNewArgoError(t *testing.T) { + cause := errors.New("test error") + err := NewArgoError("test operation", cause) + + assert.Equal(t, "Argo Rollouts", err.Component) + assert.Equal(t, "test operation", err.Operation) + assert.Equal(t, cause, err.Cause) +} + +func TestNewCiliumError(t *testing.T) { + cause := errors.New("test error") + err := NewCiliumError("test operation", cause) + + assert.Equal(t, "Cilium", err.Component) + assert.Equal(t, "test operation", err.Operation) + assert.Equal(t, cause, err.Cause) +} + +func TestNewValidationError(t *testing.T) { + err := NewValidationError("test-field", "validation failed") + + assert.Equal(t, "Validation", err.Component) + assert.Equal(t, "validate test-field", err.Operation) + assert.Equal(t, "VALIDATION_ERROR", err.ErrorCode) + assert.False(t, err.IsRetryable) + assert.Contains(t, err.Cause.Error(), "validation failed") +} + +func TestNewSecurityError(t *testing.T) { + cause := errors.New("security violation") + err := NewSecurityError("test operation", cause) + + assert.Equal(t, "Security", err.Component) + assert.Equal(t, "test operation", err.Operation) + assert.Equal(t, cause, err.Cause) + assert.Equal(t, "SECURITY_ERROR", err.ErrorCode) + assert.False(t, err.IsRetryable) +} + +func TestNewTimeoutError(t *testing.T) { + timeout := 30 * time.Second + err := NewTimeoutError("test operation", timeout) + + assert.Equal(t, "Timeout", err.Component) + assert.Equal(t, "test operation", err.Operation) + assert.Equal(t, "TIMEOUT_ERROR", err.ErrorCode) + assert.True(t, err.IsRetryable) + assert.Contains(t, err.Cause.Error(), "30s") +} + +func TestNewCommandError(t *testing.T) { + cause := errors.New("command failed") + err := NewCommandError("test-command", cause) + + assert.Equal(t, "Command", err.Component) + assert.Equal(t, "execute test-command", err.Operation) + assert.Equal(t, cause, err.Cause) + assert.Equal(t, "COMMAND_ERROR", err.ErrorCode) + assert.True(t, err.IsRetryable) +} + +func TestToolErrorChaining(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause). + WithErrorCode("TEST_ERROR"). + WithResource("Pod", "test-pod"). + WithSuggestions("suggestion 1"). + WithContext("key1", "value1"). + WithRetryable(true) + + // Test that all methods return the same instance for chaining + assert.Equal(t, "TEST_ERROR", err.ErrorCode) + assert.Equal(t, "Pod", err.ResourceType) + assert.Equal(t, "test-pod", err.ResourceName) + assert.Equal(t, []string{"suggestion 1"}, err.Suggestions) + assert.Equal(t, "value1", err.Context["key1"]) + assert.True(t, err.IsRetryable) +} + +func TestToolErrorStringRepresentation(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause) + + errorStr := err.Error() + assert.Contains(t, errorStr, "TestComponent") + assert.Contains(t, errorStr, "test operation") + assert.Contains(t, errorStr, "test error") + assert.Contains(t, errorStr, "failed") +} + +func TestToolErrorTimestamp(t *testing.T) { + before := time.Now() + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause) + after := time.Now() + + assert.True(t, err.Timestamp.After(before) || err.Timestamp.Equal(before)) + assert.True(t, err.Timestamp.Before(after) || err.Timestamp.Equal(after)) +} + +func TestToolErrorContextInitialization(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause) + + // Context should be initialized but empty + assert.NotNil(t, err.Context) + assert.Empty(t, err.Context) + + // Should be able to add to context + err = err.WithContext("test", "value") + assert.Equal(t, "value", err.Context["test"]) +} + +func TestMCPResultContainsExpectedFields(t *testing.T) { + cause := errors.New("test error") + err := NewToolError("TestComponent", "test operation", cause). + WithErrorCode("TEST_ERROR"). + WithResource("Pod", "test-pod"). + WithSuggestions("suggestion 1"). + WithContext("key1", "value1"). + WithRetryable(true) + + result := err.ToMCPResult() + + // The result should be an error result + assert.True(t, result.IsError) + + // Should have content + assert.NotEmpty(t, result.Content) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 00000000..041d499c --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,76 @@ +package logger + +import ( + "context" + "log/slog" + "os" + + "go.opentelemetry.io/otel/trace" +) + +var globalLogger *slog.Logger + +func Init() { + opts := &slog.HandlerOptions{ + Level: slog.LevelInfo, + } + + if os.Getenv("KAGENT_LOG_FORMAT") == "json" { + globalLogger = slog.New(slog.NewJSONHandler(os.Stdout, opts)) + } else { + globalLogger = slog.New(slog.NewTextHandler(os.Stdout, opts)) + } + + slog.SetDefault(globalLogger) +} + +func Get() *slog.Logger { + if globalLogger == nil { + Init() + } + return globalLogger +} + +func WithContext(ctx context.Context) *slog.Logger { + logger := Get() + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + logger = logger.With( + "trace_id", span.SpanContext().TraceID().String(), + "span_id", span.SpanContext().SpanID().String(), + ) + } + return logger +} + +func LogExecCommand(ctx context.Context, logger *slog.Logger, command string, args []string, caller string) { + logger.Info("executing command", + "command", command, + "args", args, + "caller", caller, + ) +} + +func LogExecCommandResult(ctx context.Context, logger *slog.Logger, command string, args []string, output string, err error, duration float64, caller string) { + if err != nil { + logger.Error("command execution failed", + "command", command, + "args", args, + "error", err.Error(), + "duration_seconds", duration, + "caller", caller, + ) + } else { + logger.Info("command execution successful", + "command", command, + "args", args, + "output", output, + "duration_seconds", duration, + "caller", caller, + ) + } +} + +func Sync() { + // No-op for slog, but kept for compatibility +} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go new file mode 100644 index 00000000..ad5c9889 --- /dev/null +++ b/internal/logger/logger_test.go @@ -0,0 +1,72 @@ +package logger + +import ( + "bytes" + "context" + "encoding/json" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" +) + +func TestLogExecCommand(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(slog.NewTextHandler(&buf, nil)) + + ctx := context.Background() + LogExecCommand(ctx, logger, "test-command", []string{"arg1", "arg2"}, "test.go:123") + + output := buf.String() + assert.Contains(t, output, "executing command") + assert.Contains(t, output, "test-command") + assert.Contains(t, output, "arg1") + assert.Contains(t, output, "arg2") +} + +func TestLogExecCommandResult(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(slog.NewTextHandler(&buf, nil)) + + ctx := context.Background() + LogExecCommandResult(ctx, logger, "test-command", []string{"arg1"}, "success output", nil, 1.5, "test.go:123") + assert.Contains(t, buf.String(), "command execution successful") + + buf.Reset() + LogExecCommandResult(ctx, logger, "test-command", []string{"arg1"}, "error output", assert.AnError, 0.5, "test.go:123") + assert.Contains(t, buf.String(), "command execution failed") +} + +func TestWithContextAddsTraceID(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(slog.NewJSONHandler(&buf, nil)) + + // Create a context with a mock span + tp := noop.NewTracerProvider() + ctx, span := tp.Tracer("test").Start(context.Background(), "test-span") + defer span.End() + + loggerWithTrace := logger.With("trace_id", span.SpanContext().TraceID().String()) + loggerWithTrace.InfoContext(ctx, "test message") + + var logOutput map[string]interface{} + err := json.Unmarshal(buf.Bytes(), &logOutput) + require.NoError(t, err) + + traceID := span.SpanContext().TraceID().String() + assert.Equal(t, traceID, logOutput["trace_id"]) +} + +func TestGet(t *testing.T) { + assert.NotNil(t, Get()) +} + +func TestInit(t *testing.T) { + assert.NotPanics(t, Init) +} + +func TestSync(t *testing.T) { + assert.NotPanics(t, Sync) +} diff --git a/internal/security/validation.go b/internal/security/validation.go new file mode 100644 index 00000000..6aadc383 --- /dev/null +++ b/internal/security/validation.go @@ -0,0 +1,291 @@ +package security + +import ( + "fmt" + "regexp" + "strings" +) + +// ValidationError represents a validation error +type ValidationError struct { + Field string + Message string +} + +func (e ValidationError) Error() string { + return fmt.Sprintf("validation error in field '%s': %s", e.Field, e.Message) +} + +// Common validation patterns +var ( + // K8s resource name pattern (RFC 1123) + k8sNamePattern = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`) + + // Namespace pattern + namespacePattern = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`) + + // Container image pattern + imagePattern = regexp.MustCompile(`^[a-z0-9]+(([._-][a-z0-9]+)*(/[a-z0-9]+(([._-][a-z0-9]+)*)?)*)?(:([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?))$`) + + // Path pattern (no directory traversal) + pathPattern = regexp.MustCompile(`^[a-zA-Z0-9._/-]+$`) + + // Command injection patterns to reject + commandInjectionPatterns = []*regexp.Regexp{ + regexp.MustCompile(`[;&|` + "`" + `$(){}[\]\\<>*?~!#\n\r\t]`), + regexp.MustCompile(`\.\./`), + regexp.MustCompile(`\$\{`), + regexp.MustCompile(`\$\(`), + regexp.MustCompile(`\|\|`), + regexp.MustCompile(`&&`), + } +) + +// ValidateK8sResourceName validates a Kubernetes resource name +func ValidateK8sResourceName(name string) error { + if name == "" { + return ValidationError{Field: "name", Message: "cannot be empty"} + } + + if len(name) > 63 { + return ValidationError{Field: "name", Message: "cannot exceed 63 characters"} + } + + if !k8sNamePattern.MatchString(name) { + return ValidationError{Field: "name", Message: "must follow RFC 1123 naming convention"} + } + + return nil +} + +// ValidateNamespace validates a Kubernetes namespace +func ValidateNamespace(namespace string) error { + if namespace == "" { + return nil // Empty namespace is allowed (defaults to 'default') + } + + if len(namespace) > 63 { + return ValidationError{Field: "namespace", Message: "cannot exceed 63 characters"} + } + + if !namespacePattern.MatchString(namespace) { + return ValidationError{Field: "namespace", Message: "must follow RFC 1123 naming convention"} + } + + // Reserved namespaces + reserved := []string{"kube-system", "kube-public", "kube-node-lease"} + for _, res := range reserved { + if namespace == res { + return ValidationError{Field: "namespace", Message: fmt.Sprintf("'%s' is a reserved namespace", namespace)} + } + } + + return nil +} + +// ValidateContainerImage validates a container image reference +func ValidateContainerImage(image string) error { + if image == "" { + return ValidationError{Field: "image", Message: "cannot be empty"} + } + + if len(image) > 255 { + return ValidationError{Field: "image", Message: "cannot exceed 255 characters"} + } + + if !imagePattern.MatchString(image) { + return ValidationError{Field: "image", Message: "invalid image format"} + } + + return nil +} + +// ValidateFilePath validates a file path for security +func ValidateFilePath(path string) error { + if path == "" { + return ValidationError{Field: "path", Message: "cannot be empty"} + } + + if len(path) > 4096 { + return ValidationError{Field: "path", Message: "path too long"} + } + + if strings.Contains(path, "..") { + return ValidationError{Field: "path", Message: "path traversal not allowed"} + } + + if !pathPattern.MatchString(path) { + return ValidationError{Field: "path", Message: "contains invalid characters"} + } + + return nil +} + +// ValidateCommandInput validates command inputs for injection attacks +func ValidateCommandInput(input string) error { + if input == "" { + return ValidationError{Field: "input", Message: "cannot be empty"} + } + + if len(input) > 1024 { + return ValidationError{Field: "input", Message: "input too long"} + } + + for _, pattern := range commandInjectionPatterns { + if pattern.MatchString(input) { + return ValidationError{Field: "input", Message: "potentially dangerous characters detected"} + } + } + + return nil +} + +// SanitizeInput sanitizes input strings by replacing potentially dangerous characters +func SanitizeInput(input string) string { + // Replace dangerous characters with safe alternatives + sanitized := strings.ReplaceAll(input, "\n", " ") + sanitized = strings.ReplaceAll(sanitized, "\r", " ") + sanitized = strings.ReplaceAll(sanitized, "\t", " ") + + // Replace multiple spaces with single space + spacePattern := regexp.MustCompile(`\s+`) + sanitized = spacePattern.ReplaceAllString(sanitized, " ") + + sanitized = strings.TrimSpace(sanitized) + + return sanitized +} + +// ValidateK8sLabel validates a Kubernetes label key and value +func ValidateK8sLabel(key, value string) error { + if key == "" { + return ValidationError{Field: "label_key", Message: "cannot be empty"} + } + + if len(key) > 63 { + return ValidationError{Field: "label_key", Message: "cannot exceed 63 characters"} + } + + if len(value) > 63 { + return ValidationError{Field: "label_value", Message: "cannot exceed 63 characters"} + } + + // Label key validation + labelKeyPattern := regexp.MustCompile(`^[a-z0-9A-Z]([a-z0-9A-Z._-]*[a-z0-9A-Z])?$`) + if !labelKeyPattern.MatchString(key) { + return ValidationError{Field: "label_key", Message: "invalid label key format"} + } + + // Label value validation (can be empty) + if value != "" { + labelValuePattern := regexp.MustCompile(`^[a-z0-9A-Z]([a-z0-9A-Z._-]*[a-z0-9A-Z])?$`) + if !labelValuePattern.MatchString(value) { + return ValidationError{Field: "label_value", Message: "invalid label value format"} + } + } + + return nil +} + +// ValidatePromQLQuery validates a PromQL query for basic security +func ValidatePromQLQuery(query string) error { + if query == "" { + return ValidationError{Field: "query", Message: "cannot be empty"} + } + + if len(query) > 8192 { + return ValidationError{Field: "query", Message: "query too long"} + } + + // Basic PromQL validation - no shell commands + dangerousPatterns := []string{ + "`", "$", "$(", "${", "&&", "||", ";", "|", ">", "<", "&", + } + + for _, pattern := range dangerousPatterns { + if strings.Contains(query, pattern) { + return ValidationError{Field: "query", Message: "potentially dangerous characters in query"} + } + } + + return nil +} + +// ValidateYAMLContent validates YAML content for basic security +func ValidateYAMLContent(content string) error { + if content == "" { + return ValidationError{Field: "content", Message: "cannot be empty"} + } + + if len(content) > 1024*1024 { // 1MB limit + return ValidationError{Field: "content", Message: "content too large"} + } + + // Check for potentially dangerous YAML content + dangerousPatterns := []string{ + "!!python/object/apply", + "!!python/object/new", + "!!python/object", + "__import__", + "eval(", + "exec(", + } + + for _, pattern := range dangerousPatterns { + if strings.Contains(content, pattern) { + return ValidationError{Field: "content", Message: "potentially dangerous YAML content detected"} + } + } + + return nil +} + +// ValidateHelmReleaseName validates a Helm release name +func ValidateHelmReleaseName(name string) error { + if name == "" { + return ValidationError{Field: "release_name", Message: "cannot be empty"} + } + + if len(name) > 53 { + return ValidationError{Field: "release_name", Message: "cannot exceed 53 characters"} + } + + // Helm release name pattern + helmNamePattern := regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`) + if !helmNamePattern.MatchString(name) { + return ValidationError{Field: "release_name", Message: "must follow DNS naming convention"} + } + + return nil +} + +// ValidateURL validates a URL for basic security +func ValidateURL(url string) error { + if url == "" { + return ValidationError{Field: "url", Message: "cannot be empty"} + } + + if len(url) > 2048 { + return ValidationError{Field: "url", Message: "URL too long"} + } + + // Basic URL validation + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + return ValidationError{Field: "url", Message: "must start with http:// or https://"} + } + + // Check for dangerous URL patterns + dangerousPatterns := []string{ + "javascript:", "data:", "file:", "ftp:", + "", true}, + {"too long path", string(make([]byte, 5000)), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateFilePath(tt.input) + if tt.expectError && err == nil { + t.Errorf("Expected error for input %q, but got none", tt.input) + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error for input %q: %v", tt.input, err) + } + }) + } +} + +func TestValidateCommandInput(t *testing.T) { + tests := []struct { + name string + input string + expectError bool + }{ + {"valid input", "my-service", false}, + {"empty input", "", true}, + {"command injection", "test; rm -rf /", true}, + {"pipe injection", "test | cat /etc/passwd", true}, + {"backtick injection", "test`whoami`", true}, + {"variable expansion", "test${USER}", true}, + {"too long input", string(make([]byte, 2000)), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateCommandInput(tt.input) + if tt.expectError && err == nil { + t.Errorf("Expected error for input %q, but got none", tt.input) + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error for input %q: %v", tt.input, err) + } + }) + } +} + +func TestSanitizeInput(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"clean input", "hello world", "hello world"}, + {"with newlines", "hello\nworld", "hello world"}, + {"with tabs", "hello\tworld", "hello world"}, + {"with carriage returns", "hello\rworld", "hello world"}, + {"with spaces", " hello world ", "hello world"}, + {"mixed whitespace", "\n\t hello world \r\n", "hello world"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := SanitizeInput(tt.input) + if result != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestValidateK8sLabel(t *testing.T) { + tests := []struct { + name string + key string + value string + expectError bool + }{ + {"valid label", "app", "nginx", false}, + {"valid label with dash", "app-version", "1.0", false}, + {"valid label with underscore", "app_name", "nginx", false}, + {"empty key", "", "value", true}, + {"empty value", "key", "", false}, // Empty value is allowed + {"too long key", string(make([]byte, 70)), "value", true}, + {"too long value", "key", string(make([]byte, 70)), true}, + {"invalid key characters", "app/name", "nginx", true}, + {"invalid value characters", "app", "nginx/web", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateK8sLabel(tt.key, tt.value) + if tt.expectError && err == nil { + t.Errorf("Expected error for key %q, value %q, but got none", tt.key, tt.value) + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error for key %q, value %q: %v", tt.key, tt.value, err) + } + }) + } +} + +func TestValidatePromQLQuery(t *testing.T) { + tests := []struct { + name string + input string + expectError bool + }{ + {"valid query", "up{job=\"prometheus\"}", false}, + {"valid aggregation", "sum(rate(http_requests_total[5m]))", false}, + {"empty query", "", true}, + {"command injection", "up; rm -rf /", true}, + {"backtick injection", "up`whoami`", true}, + {"variable expansion", "up${USER}", true}, + {"too long query", string(make([]byte, 10000)), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidatePromQLQuery(tt.input) + if tt.expectError && err == nil { + t.Errorf("Expected error for input %q, but got none", tt.input) + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error for input %q: %v", tt.input, err) + } + }) + } +} + +func TestValidateYAMLContent(t *testing.T) { + tests := []struct { + name string + input string + expectError bool + }{ + {"valid YAML", "apiVersion: v1\nkind: Pod", false}, + {"empty content", "", true}, + {"python object", "!!python/object/apply", true}, + {"python import", "__import__('os').system('rm -rf /')", true}, + {"eval injection", "eval('print(1)')", true}, + {"too large content", string(make([]byte, 2*1024*1024)), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateYAMLContent(tt.input) + if tt.expectError && err == nil { + t.Errorf("Expected error for input %q, but got none", tt.input) + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error for input %q: %v", tt.input, err) + } + }) + } +} + +func TestValidateHelmReleaseName(t *testing.T) { + tests := []struct { + name string + input string + expectError bool + }{ + {"valid release name", "my-release", false}, + {"valid with numbers", "release-123", false}, + {"empty name", "", true}, + {"too long name", "this-is-a-very-long-release-name-that-exceeds-the-maximum-allowed-length-of-53-characters", true}, + {"invalid characters", "my_release", true}, + {"starts with dash", "-release", true}, + {"ends with dash", "release-", true}, + {"uppercase", "Release", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateHelmReleaseName(tt.input) + if tt.expectError && err == nil { + t.Errorf("Expected error for input %q, but got none", tt.input) + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error for input %q: %v", tt.input, err) + } + }) + } +} + +func TestValidateURL(t *testing.T) { + tests := []struct { + name string + input string + expectError bool + }{ + {"valid http URL", "http://example.com", false}, + {"valid https URL", "https://example.com/path", false}, + {"empty URL", "", true}, + {"invalid protocol", "ftp://example.com", true}, + {"javascript injection", "javascript:alert('xss')", true}, + {"data URL", "data:text/html,", true}, + {"too long URL", "https://" + string(make([]byte, 3000)), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateURL(tt.input) + if tt.expectError && err == nil { + t.Errorf("Expected error for input %q, but got none", tt.input) + } + if !tt.expectError && err != nil { + t.Errorf("Unexpected error for input %q: %v", tt.input, err) + } + }) + } +} + +func TestValidationError(t *testing.T) { + err := ValidationError{ + Field: "test_field", + Message: "test message", + } + + expected := "validation error in field 'test_field': test message" + if err.Error() != expected { + t.Errorf("Expected error message %q, got %q", expected, err.Error()) + } +} diff --git a/internal/telemetry/config.go b/internal/telemetry/config.go new file mode 100644 index 00000000..56b266e8 --- /dev/null +++ b/internal/telemetry/config.go @@ -0,0 +1,74 @@ +package telemetry + +import ( + "os" + "strconv" + "strings" + "sync" +) + +// Telemetry holds all telemetry-related configuration. +type Telemetry struct { + ServiceName string + ServiceVersion string + Environment string + Endpoint string + Protocol string + SamplingRatio float64 + Insecure bool + Disabled bool +} + +// Config holds all application configuration. +type Config struct { + Telemetry Telemetry +} + +var ( + once sync.Once + config *Config +) + +// LoadOtelCfg initializes and returns the application configuration. +func LoadOtelCfg() *Config { + once.Do(func() { + config = &Config{ + Telemetry: Telemetry{ + ServiceName: getEnv("OTEL_SERVICE_NAME", "kagent-tools"), + ServiceVersion: getEnv("OTEL_SERVICE_VERSION", "dev"), + Environment: getEnv("OTEL_ENVIRONMENT", "development"), + Endpoint: getEnv("OTEL_EXPORTER_OTLP_ENDPOINT", ""), + Protocol: getEnv("OTEL_EXPORTER_OTLP_PROTOCOL", "auto"), + SamplingRatio: getEnvFloat("OTEL_TRACES_SAMPLER_ARG", 1.0), + Insecure: getEnvBool("OTEL_EXPORTER_OTLP_TRACES_INSECURE", false), + Disabled: getEnvBool("OTEL_SDK_DISABLED", false), + }, + } + }) + return config +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +func getEnvFloat(key string, fallback float64) float64 { + if valueStr, ok := os.LookupEnv(key); ok { + if value, err := strconv.ParseFloat(valueStr, 64); err == nil { + return value + } + } + return fallback +} + +func getEnvBool(key string, fallback bool) bool { + if valueStr, ok := os.LookupEnv(key); ok { + if value, err := strconv.ParseBool(strings.ToLower(valueStr)); err == nil { + return value + } + } + return fallback +} diff --git a/internal/telemetry/config_test.go b/internal/telemetry/config_test.go new file mode 100644 index 00000000..fe6454b5 --- /dev/null +++ b/internal/telemetry/config_test.go @@ -0,0 +1,49 @@ +package telemetry + +import ( + "os" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoad(t *testing.T) { + // Reset singleton for testing + once = sync.Once{} + config = nil + + os.Setenv("OTEL_SERVICE_NAME", "test-service") + os.Setenv("OTEL_EXPORTER_OTLP_TRACES_INSECURE", "true") + defer func() { + os.Unsetenv("OTEL_SERVICE_NAME") + os.Unsetenv("OTEL_EXPORTER_OTLP_TRACES_INSECURE") + }() + + cfg := LoadOtelCfg() + assert.Equal(t, "test-service", cfg.Telemetry.ServiceName) + assert.True(t, cfg.Telemetry.Insecure) +} + +func TestLoadDefaults(t *testing.T) { + // Reset singleton for testing + once = sync.Once{} + config = nil + + cfg := LoadOtelCfg() + assert.Equal(t, "kagent-tools", cfg.Telemetry.ServiceName) + assert.False(t, cfg.Telemetry.Insecure) + assert.Equal(t, 1.0, cfg.Telemetry.SamplingRatio) +} + +func TestLoadDevelopmentSampling(t *testing.T) { + // Reset singleton for testing + once = sync.Once{} + config = nil + + os.Setenv("OTEL_ENVIRONMENT", "development") + defer os.Unsetenv("OTEL_ENVIRONMENT") + + cfg := LoadOtelCfg() + assert.Equal(t, 1.0, cfg.Telemetry.SamplingRatio) +} diff --git a/internal/telemetry/middleware.go b/internal/telemetry/middleware.go new file mode 100644 index 00000000..720a99b8 --- /dev/null +++ b/internal/telemetry/middleware.go @@ -0,0 +1,179 @@ +package telemetry + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +type ToolHandler func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + +// contextKey is used for storing HTTP context in the request context +type contextKey string + +const ( + HTTPHeadersKey contextKey = "http_headers" + TraceIDKey contextKey = "trace_id" + SpanIDKey contextKey = "span_id" +) + +// HTTPMiddleware wraps an HTTP handler to extract headers and propagate context +func HTTPMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Extract OpenTelemetry context from HTTP headers + propagator := otel.GetTextMapPropagator() + ctx = propagator.Extract(ctx, propagation.HeaderCarrier(r.Header)) + + // Store relevant HTTP headers in context for tool handlers + headers := make(map[string]string) + for name, values := range r.Header { + if len(values) > 0 { + // Store important headers for debugging/tracing + switch name { + case "X-Request-ID", "X-Correlation-ID", "X-Trace-ID", + "User-Agent", "Authorization", "X-Forwarded-For": + headers[name] = values[0] + } + } + } + + // Add headers to context + ctx = context.WithValue(ctx, HTTPHeadersKey, headers) + + // Extract trace information if available + span := trace.SpanFromContext(ctx) + if span.SpanContext().HasTraceID() { + ctx = context.WithValue(ctx, TraceIDKey, span.SpanContext().TraceID().String()) + ctx = context.WithValue(ctx, SpanIDKey, span.SpanContext().SpanID().String()) + } + + // Call next handler with enhanced context + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// ExtractHTTPHeaders retrieves HTTP headers from context +func ExtractHTTPHeaders(ctx context.Context) map[string]string { + if headers, ok := ctx.Value(HTTPHeadersKey).(map[string]string); ok { + return headers + } + return make(map[string]string) +} + +// ExtractTraceInfo retrieves trace information from context +func ExtractTraceInfo(ctx context.Context) (traceID, spanID string) { + if tid, ok := ctx.Value(TraceIDKey).(string); ok { + traceID = tid + } + if sid, ok := ctx.Value(SpanIDKey).(string); ok { + spanID = sid + } + return traceID, spanID +} + +func WithTracing(toolName string, handler ToolHandler) ToolHandler { + return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + tracer := otel.Tracer("kagent-tools/mcp") + + spanName := fmt.Sprintf("mcp.tool.%s", toolName) + ctx, span := tracer.Start(ctx, spanName) + defer span.End() + + // Extract HTTP headers from context and add as span attributes + headers := ExtractHTTPHeaders(ctx) + for key, value := range headers { + span.SetAttributes(attribute.String(fmt.Sprintf("http.header.%s", key), value)) + } + + // Extract parent trace information + parentTraceID, parentSpanID := ExtractTraceInfo(ctx) + if parentTraceID != "" { + span.SetAttributes( + attribute.String("http.parent_trace_id", parentTraceID), + attribute.String("http.parent_span_id", parentSpanID), + ) + } + + span.SetAttributes( + attribute.String("mcp.tool.name", toolName), + attribute.String("mcp.request.id", request.Params.Name), + ) + + if request.Params.Arguments != nil { + if argsJSON, err := json.Marshal(request.Params.Arguments); err == nil { + span.SetAttributes(attribute.String("mcp.request.arguments", string(argsJSON))) + } + } + + span.AddEvent("tool.execution.start") + startTime := time.Now() + + result, err := handler(ctx, request) + + duration := time.Since(startTime) + span.SetAttributes(attribute.Float64("mcp.tool.duration_seconds", duration.Seconds())) + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + span.AddEvent("tool.execution.error", trace.WithAttributes( + attribute.String("error.message", err.Error()), + )) + } else { + span.SetStatus(codes.Ok, "tool execution completed successfully") + span.AddEvent("tool.execution.success") + + if result != nil { + span.SetAttributes(attribute.Bool("mcp.result.is_error", result.IsError)) + if result.Content != nil { + span.SetAttributes(attribute.Int("mcp.result.content_count", len(result.Content))) + } + } + } + + return result, err + } +} + +func StartSpan(ctx context.Context, operationName string, attrs ...attribute.KeyValue) (context.Context, trace.Span) { + tracer := otel.Tracer("kagent-tools") + ctx, span := tracer.Start(ctx, operationName) + + if len(attrs) > 0 { + span.SetAttributes(attrs...) + } + + return ctx, span +} + +func RecordError(span trace.Span, err error, message string) { + span.RecordError(err) + span.SetStatus(codes.Error, message) +} + +func RecordSuccess(span trace.Span, message string) { + span.SetStatus(codes.Ok, message) +} + +func AddEvent(span trace.Span, name string, attrs ...attribute.KeyValue) { + span.AddEvent(name, trace.WithAttributes(attrs...)) +} + +// AdaptToolHandler adapts a telemetry.ToolHandler to a server.ToolHandlerFunc. +func AdaptToolHandler(th ToolHandler) server.ToolHandlerFunc { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return th(ctx, req) + } +} diff --git a/internal/telemetry/middleware_test.go b/internal/telemetry/middleware_test.go new file mode 100644 index 00000000..bcbf494c --- /dev/null +++ b/internal/telemetry/middleware_test.go @@ -0,0 +1,801 @@ +package telemetry + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace/noop" +) + +// InMemoryExporter is a simple in-memory exporter for testing +type InMemoryExporter struct { + spans []trace.ReadOnlySpan +} + +func (e *InMemoryExporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error { + e.spans = append(e.spans, spans...) + return nil +} + +func (e *InMemoryExporter) Shutdown(ctx context.Context) error { + return nil +} + +func (e *InMemoryExporter) GetSpans() []trace.ReadOnlySpan { + return e.spans +} + +// setupTracing initializes OpenTelemetry with in-memory exporter for testing +func setupTracing() (*trace.TracerProvider, *InMemoryExporter) { + exporter := &InMemoryExporter{} + provider := trace.NewTracerProvider( + trace.WithSampler(trace.AlwaysSample()), + trace.WithSpanProcessor(trace.NewSimpleSpanProcessor(exporter)), + ) + otel.SetTracerProvider(provider) + return provider, exporter +} + +func TestWithTracing(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Create a test handler + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + textContent := mcp.NewTextContent("test response") + return &mcp.CallToolResult{ + IsError: false, + Content: []mcp.Content{textContent}, + }, nil + } + + // Wrap with tracing + tracedHandler := WithTracing("test-tool", testHandler) + + // Create test request + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "test-tool", + Arguments: map[string]interface{}{ + "param1": "value1", + "param2": 42, + }, + }, + } + + // Execute the handler + result, err := tracedHandler(context.Background(), request) + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify result + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Len(t, result.Content, 1) + textContent, ok := mcp.AsTextContent(result.Content[0]) + require.True(t, ok) + assert.Equal(t, "test response", textContent.Text) + + // Verify span was created + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "mcp.tool.test-tool", span.Name()) + assert.Equal(t, codes.Ok, span.Status().Code) + // Note: SDK may not preserve description in test environment + // assert.Equal(t, "tool execution completed successfully", span.Status().Description) + + // Verify attributes + attributes := span.Attributes() + hasToolName := false + hasRequestID := false + hasIsError := false + hasContentCount := false + + for _, attr := range attributes { + if attr.Key == "mcp.tool.name" && attr.Value.AsString() == "test-tool" { + hasToolName = true + } + if attr.Key == "mcp.request.id" && attr.Value.AsString() == "test-tool" { + hasRequestID = true + } + if attr.Key == "mcp.result.is_error" && attr.Value.AsBool() == false { + hasIsError = true + } + if attr.Key == "mcp.result.content_count" && attr.Value.AsInt64() == 1 { + hasContentCount = true + } + } + + assert.True(t, hasToolName) + assert.True(t, hasRequestID) + assert.True(t, hasIsError) + assert.True(t, hasContentCount) + + // Verify events + events := span.Events() + assert.Len(t, events, 2) + assert.Equal(t, "tool.execution.start", events[0].Name) + assert.Equal(t, "tool.execution.success", events[1].Name) +} + +func TestWithTracingError(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Create a test handler that returns an error + testError := errors.New("test error") + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return nil, testError + } + + // Wrap with tracing + tracedHandler := WithTracing("test-tool", testHandler) + + // Create test request + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "test-tool", + }, + } + + // Execute the handler + result, err := tracedHandler(context.Background(), request) + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify result + assert.Error(t, err) + assert.Equal(t, testError, err) + assert.Nil(t, result) + + // Verify span was created with error + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "mcp.tool.test-tool", span.Name()) + assert.Equal(t, codes.Error, span.Status().Code) + // Note: SDK may not preserve description in test environment + // assert.Equal(t, "test error", span.Status().Description) + + // Verify events - span.RecordError() adds an "exception" event, plus our custom events + events := span.Events() + assert.Len(t, events, 3) + assert.Equal(t, "tool.execution.start", events[0].Name) + assert.Equal(t, "exception", events[1].Name) // Added by span.RecordError() + assert.Equal(t, "tool.execution.error", events[2].Name) +} + +func TestWithTracingErrorResult(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Create a test handler that returns an error result + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + textContent := mcp.NewTextContent("error occurred") + return &mcp.CallToolResult{ + IsError: true, + Content: []mcp.Content{textContent}, + }, nil + } + + // Wrap with tracing + tracedHandler := WithTracing("test-tool", testHandler) + + // Create test request + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "test-tool", + }, + } + + // Execute the handler + result, err := tracedHandler(context.Background(), request) + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify result + require.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + + // Verify span was created successfully (no error from handler) + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "mcp.tool.test-tool", span.Name()) + assert.Equal(t, codes.Ok, span.Status().Code) + + // Verify attributes + attributes := span.Attributes() + hasIsError := false + hasContentCount := false + + for _, attr := range attributes { + if attr.Key == "mcp.result.is_error" && attr.Value.AsBool() == true { + hasIsError = true + } + if attr.Key == "mcp.result.content_count" && attr.Value.AsInt64() == 1 { + hasContentCount = true + } + } + + assert.True(t, hasIsError) + assert.True(t, hasContentCount) +} + +func TestWithTracingWithArguments(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Create a test handler + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + textContent := mcp.NewTextContent("test response") + return &mcp.CallToolResult{ + IsError: false, + Content: []mcp.Content{textContent}, + }, nil + } + + // Wrap with tracing + tracedHandler := WithTracing("test-tool", testHandler) + + // Create test request with arguments + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "test-tool", + Arguments: map[string]interface{}{ + "string_param": "hello", + "number_param": 42, + "bool_param": true, + "array_param": []interface{}{"a", "b", "c"}, + "object_param": map[string]interface{}{ + "nested": "value", + }, + }, + }, + } + + // Execute the handler + result, err := tracedHandler(context.Background(), request) + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify result + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Verify span was created + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "mcp.tool.test-tool", span.Name()) + + // Verify that arguments were added as an attribute (they are JSON-encoded) + attributes := span.Attributes() + hasArguments := false + + for _, attr := range attributes { + if attr.Key == "mcp.request.arguments" { + hasArguments = true + // Arguments should be JSON-encoded + assert.NotEmpty(t, attr.Value.AsString()) + } + } + + assert.True(t, hasArguments) +} + +func TestWithTracingNilArguments(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Create a test handler + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + textContent := mcp.NewTextContent("test response") + return &mcp.CallToolResult{ + IsError: false, + Content: []mcp.Content{textContent}, + }, nil + } + + // Wrap with tracing + tracedHandler := WithTracing("test-tool", testHandler) + + // Create test request without arguments + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "test-tool", + }, + } + + // Execute the handler + result, err := tracedHandler(context.Background(), request) + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify result + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Verify span was created + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "mcp.tool.test-tool", span.Name()) +} + +func TestStartSpan(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Start a span + _, span := StartSpan(context.Background(), "test-span", + attribute.String("key1", "value1"), + attribute.Int("key2", 42), + ) + + // End the span + span.End() + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify span was created + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + resultSpan := spans[0] + assert.Equal(t, "test-span", resultSpan.Name()) +} + +func TestStartSpanNoAttributes(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Start a span without attributes + _, span := StartSpan(context.Background(), "test-span") + + // End the span + span.End() + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify span was created + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + resultSpan := spans[0] + assert.Equal(t, "test-span", resultSpan.Name()) +} + +func TestRecordError(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Start a span + _, span := StartSpan(context.Background(), "test-span") + + // Record an error + testError := errors.New("test error") + RecordError(span, testError, "test error") + + // End the span + span.End() + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify span was created with error + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + resultSpan := spans[0] + assert.Equal(t, "test-span", resultSpan.Name()) + assert.Equal(t, codes.Error, resultSpan.Status().Code) + assert.Equal(t, "test error", resultSpan.Status().Description) +} + +func TestRecordSuccess(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Start a span + _, span := StartSpan(context.Background(), "test-span") + + // Record success + RecordSuccess(span, "operation completed successfully") + + // End the span + span.End() + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify span was created with success + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + resultSpan := spans[0] + assert.Equal(t, "test-span", resultSpan.Name()) + assert.Equal(t, codes.Ok, resultSpan.Status().Code) + // Note: SDK may not preserve description in test environment + // assert.Equal(t, "operation completed successfully", resultSpan.Status().Description) +} + +func TestAddEvent(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Start a span + _, span := StartSpan(context.Background(), "test-span") + + // Add an event + AddEvent(span, "test-event", + attribute.String("event_key", "event_value"), + attribute.Int("event_num", 123), + ) + + // End the span + span.End() + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify span was created with event + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + resultSpan := spans[0] + assert.Equal(t, "test-span", resultSpan.Name()) + + // Verify event + events := resultSpan.Events() + assert.Len(t, events, 1) + assert.Equal(t, "test-event", events[0].Name) +} + +func TestAddEventNoAttributes(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Start a span + _, span := StartSpan(context.Background(), "test-span") + + // Add an event without attributes + AddEvent(span, "test-event") + + // End the span + span.End() + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify span was created with event + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + resultSpan := spans[0] + assert.Equal(t, "test-span", resultSpan.Name()) + + // Verify event + events := resultSpan.Events() + assert.Len(t, events, 1) + assert.Equal(t, "test-event", events[0].Name) +} + +func TestAdaptToolHandler(t *testing.T) { + // Create a test handler + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + textContent := mcp.NewTextContent("test response") + return &mcp.CallToolResult{ + IsError: false, + Content: []mcp.Content{textContent}, + }, nil + } + + // Adapt the handler + adapted := AdaptToolHandler(testHandler) + + // Create test request + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "test-tool", + }, + } + + // Execute the adapted handler + result, err := adapted(context.Background(), request) + + // Verify result + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Len(t, result.Content, 1) + textContent, ok := mcp.AsTextContent(result.Content[0]) + require.True(t, ok) + assert.Equal(t, "test response", textContent.Text) +} + +func TestWithTracingNilResult(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Create a test handler that returns nil result + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return nil, nil + } + + // Wrap with tracing + tracedHandler := WithTracing("test-tool", testHandler) + + // Create test request + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "test-tool", + }, + } + + // Execute the handler + result, err := tracedHandler(context.Background(), request) + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify result + require.NoError(t, err) + assert.Nil(t, result) + + // Verify span was created + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "mcp.tool.test-tool", span.Name()) + assert.Equal(t, codes.Ok, span.Status().Code) +} + +func TestWithTracingNoContent(t *testing.T) { + // Initialize OpenTelemetry + provider, exporter := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Create a test handler that returns result with no content + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return &mcp.CallToolResult{ + IsError: false, + Content: []mcp.Content{}, + }, nil + } + + // Wrap with tracing + tracedHandler := WithTracing("test-tool", testHandler) + + // Create test request + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "test-tool", + }, + } + + // Execute the handler + result, err := tracedHandler(context.Background(), request) + + // Force flush to ensure spans are exported + if err := provider.ForceFlush(context.Background()); err != nil { + t.Errorf("Failed to flush provider: %v", err) + } + + // Verify result + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Len(t, result.Content, 0) + + // Verify span was created + spans := exporter.GetSpans() + assert.Len(t, spans, 1) + + span := spans[0] + assert.Equal(t, "mcp.tool.test-tool", span.Name()) + assert.Equal(t, codes.Ok, span.Status().Code) + + // Verify attributes + attributes := span.Attributes() + hasContentCount := false + + for _, attr := range attributes { + if attr.Key == "mcp.result.content_count" && attr.Value.AsInt64() == 0 { + hasContentCount = true + } + } + + assert.True(t, hasContentCount) +} + +func TestWithTracingNoopTracer(t *testing.T) { + // Set up noop tracer provider + otel.SetTracerProvider(noop.NewTracerProvider()) + + // Create a test handler + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + textContent := mcp.NewTextContent("test response") + return &mcp.CallToolResult{ + IsError: false, + Content: []mcp.Content{textContent}, + }, nil + } + + // Wrap with tracing + tracedHandler := WithTracing("test-tool", testHandler) + + // Create test request + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "test-tool", + }, + } + + // Execute the handler + result, err := tracedHandler(context.Background(), request) + + // Verify result (should work normally with noop tracer) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Len(t, result.Content, 1) + textContent, ok := mcp.AsTextContent(result.Content[0]) + require.True(t, ok) + assert.Equal(t, "test response", textContent.Text) +} + +func TestWithTracingPerformance(t *testing.T) { + // Initialize OpenTelemetry + provider, _ := setupTracing() + defer func() { + if err := provider.Shutdown(context.Background()); err != nil { + t.Errorf("Failed to shutdown provider: %v", err) + } + }() + + // Create a test handler + testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + textContent := mcp.NewTextContent("test response") + return &mcp.CallToolResult{ + IsError: false, + Content: []mcp.Content{textContent}, + }, nil + } + + // Wrap with tracing + tracedHandler := WithTracing("test-tool", testHandler) + + // Create test request + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "test-tool", + }, + } + + // Time execution + start := time.Now() + for i := 0; i < 100; i++ { + _, err := tracedHandler(context.Background(), request) + require.NoError(t, err) + } + duration := time.Since(start) + + // Verify performance is reasonable (should complete in less than 1 second) + assert.Less(t, duration, time.Second) +} diff --git a/internal/telemetry/tracing.go b/internal/telemetry/tracing.go new file mode 100644 index 00000000..6b6f7208 --- /dev/null +++ b/internal/telemetry/tracing.go @@ -0,0 +1,282 @@ +package telemetry + +import ( + "context" + "fmt" + "net/url" + "os" + "strings" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.32.0" + "go.opentelemetry.io/otel/trace/noop" + + "github.com/kagent-dev/tools/internal/logger" +) + +// Standard OpenTelemetry environment variable names +// These follow the official OTLP specification +const ( + // Service identification + OtelServiceName = "OTEL_SERVICE_NAME" + OtelServiceVersion = "OTEL_SERVICE_VERSION" + OtelEnvironment = "OTEL_ENVIRONMENT" // Custom extension, not in official spec + + // OTLP Exporter configuration + OtelExporterOtlpEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT" + OtelExporterOtlpProtocol = "OTEL_EXPORTER_OTLP_PROTOCOL" + OtelExporterOtlpHeaders = "OTEL_EXPORTER_OTLP_HEADERS" + + // Trace-specific OTLP configuration + OtelExporterOtlpTracesInsecure = "OTEL_EXPORTER_OTLP_TRACES_INSECURE" + + // Sampling configuration + OtelTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG" + + // SDK control + OtelSdkDisabled = "OTEL_SDK_DISABLED" +) + +// OTLP Protocol constants +const ( + ProtocolGRPC = "grpc" + ProtocolHTTP = "http/protobuf" + ProtocolAuto = "auto" // Custom extension for automatic protocol detection +) + +// Standard OTLP port numbers +// These are the official OTLP default ports as per OpenTelemetry specification +const ( + DefaultOtlpGrpcPort = "4317" // Standard OTLP/gRPC port + DefaultOtlpHttpPort = "4318" // Standard OTLP/HTTP port +) + +// Default endpoint paths +const ( + DefaultHttpTracesPath = "/v1/traces" +) + +// SetupOTelSDK initializes the OpenTelemetry SDK +func SetupOTelSDK(ctx context.Context) error { + log := logger.WithContext(ctx) + cfg := LoadOtelCfg() + telemetryConfig := cfg.Telemetry + + // If tracing is disabled, set a no-op tracer provider and return. + // This prevents further initialization and ensures no traces are exported. + if cfg.Telemetry.Disabled { + otel.SetTracerProvider(noop.NewTracerProvider()) + return nil + } + + res, err := resource.New(ctx, + resource.WithDetectors(), // Detectors for cloud provider, k8s, etc. + resource.WithAttributes( + semconv.ServiceNameKey.String(telemetryConfig.ServiceName), + semconv.ServiceVersionKey.String(telemetryConfig.ServiceVersion), + attribute.String("deployment.environment", telemetryConfig.Environment), + ), + ) + if err != nil { + log.Error("failed to create resource", "error", err) + return fmt.Errorf("failed to create resource: %w", err) + } + + // Set up propagator + prop := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) + otel.SetTextMapPropagator(prop) + + exporter, err := createExporter(ctx, &telemetryConfig) + if err != nil { + log.Error("failed to create exporter", "error", err) + return fmt.Errorf("failed to create exporter: %w", err) + } + + // Set up trace provider + tracerProvider, err := newTracerProvider(ctx, &telemetryConfig, exporter, res) + if err != nil { + log.Error("failed to create tracer provider", "error", err) + return fmt.Errorf("failed to create tracer provider: %w", err) + } + otel.SetTracerProvider(tracerProvider) + + log.Info("OpenTelemetry SDK successfully initialized") + //start goroutine and wait for ctx cancellation + go func() { + <-ctx.Done() + if err := tracerProvider.Shutdown(ctx); err != nil { + log.Error("failed to shutdown tracer provider", "error", err) + } else { + log.Info("OpenTelemetry SDK shutdown successfully") + } + }() + return nil +} + +// newTracerProvider creates a new trace provider +func newTracerProvider(ctx context.Context, cfg *Telemetry, exporter sdktrace.SpanExporter, res *resource.Resource) (*sdktrace.TracerProvider, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + + sampler := sdktrace.AlwaysSample() + + tp := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sampler), + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(res), + ) + return tp, nil +} + +// createExporter creates a OTLP exporter +func createExporter(ctx context.Context, cfg *Telemetry) (sdktrace.SpanExporter, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + + if cfg.Endpoint == "" { + return stdouttrace.New(stdouttrace.WithPrettyPrint()) + } + + // Determine protocol + protocol := cfg.Protocol + if protocol == ProtocolAuto || protocol == "" { + protocol = detectProtocol(cfg.Endpoint) + } + + switch strings.ToLower(protocol) { + case ProtocolGRPC: + return createGRPCExporter(ctx, cfg) + case ProtocolHTTP: + return createHTTPExporter(ctx, cfg) + default: + return nil, fmt.Errorf("unsupported protocol: %s (supported: %s, %s)", protocol, ProtocolGRPC, ProtocolHTTP) + } +} + +// detectProtocol determines the protocol based on the endpoint URL +func detectProtocol(endpoint string) string { + // Parse URL to extract port + if parsedURL, err := url.Parse(endpoint); err == nil { + port := parsedURL.Port() + if port == "" { + // Check for default ports in hostname + if strings.Contains(parsedURL.Host, ":"+DefaultOtlpGrpcPort) { + return ProtocolGRPC + } + if strings.Contains(parsedURL.Host, ":"+DefaultOtlpHttpPort) { + return ProtocolHTTP + } + } else { + switch port { + case DefaultOtlpGrpcPort: + return ProtocolGRPC + case DefaultOtlpHttpPort: + return ProtocolHTTP + } + } + } + + // Check if endpoint contains port info directly + if strings.Contains(endpoint, ":"+DefaultOtlpGrpcPort) { + return ProtocolGRPC + } + if strings.Contains(endpoint, ":"+DefaultOtlpHttpPort) { + return ProtocolHTTP + } + + // Default to HTTP for backward compatibility + return ProtocolHTTP +} + +// createGRPCExporter creates a gRPC OTLP exporter +func createGRPCExporter(ctx context.Context, cfg *Telemetry) (sdktrace.SpanExporter, error) { + opts := []otlptracegrpc.Option{ + otlptracegrpc.WithEndpoint(normalizeGRPCEndpoint(cfg.Endpoint)), + otlptracegrpc.WithTimeout(30 * time.Second), + } + + // Use insecure connection if explicitly configured + if cfg.Insecure { + opts = append(opts, otlptracegrpc.WithInsecure()) + } + + if authToken := os.Getenv(OtelExporterOtlpHeaders); authToken != "" { + opts = append(opts, otlptracegrpc.WithHeaders(parseHeaders(authToken))) + } + + return otlptracegrpc.New(ctx, opts...) +} + +// createHTTPExporter creates an HTTP OTLP exporter +func createHTTPExporter(ctx context.Context, cfg *Telemetry) (sdktrace.SpanExporter, error) { + opts := []otlptracehttp.Option{ + otlptracehttp.WithEndpointURL(normalizeHTTPEndpoint(cfg.Endpoint, cfg.Insecure)), + otlptracehttp.WithTimeout(30 * time.Second), + } + + // Use insecure connection if explicitly configured + if cfg.Insecure { + opts = append(opts, otlptracehttp.WithInsecure()) + } + + if authToken := os.Getenv(OtelExporterOtlpHeaders); authToken != "" { + opts = append(opts, otlptracehttp.WithHeaders(parseHeaders(authToken))) + } + + return otlptracehttp.New(ctx, opts...) +} + +// normalizeGRPCEndpoint normalizes the endpoint for gRPC usage +func normalizeGRPCEndpoint(endpoint string) string { + if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { + return endpoint + } + + u, err := url.Parse(endpoint) + if err != nil { + return endpoint // Should not happen with the check above, but as a safeguard + } + + return u.Host + u.Path +} + +// normalizeHTTPEndpoint normalizes the endpoint for HTTP usage +func normalizeHTTPEndpoint(endpoint string, insecure bool) string { + // Ensure we have a proper HTTP URL + if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { + // Use HTTP if insecure is true or if endpoint contains localhost/127.0.0.1/docker.internal + if insecure || strings.Contains(endpoint, "localhost") || strings.Contains(endpoint, "127.0.0.1") || strings.Contains(endpoint, "docker.internal") { + endpoint = "http://" + endpoint + } else { + endpoint = "https://" + endpoint + } + } + + // Add /v1/traces suffix if not present + if !strings.HasSuffix(endpoint, DefaultHttpTracesPath) { + endpoint = strings.TrimSuffix(endpoint, "/") + DefaultHttpTracesPath + } + + return endpoint +} + +// parseHeaders parses a comma-separated string of headers into a map +func parseHeaders(headers string) map[string]string { + headerMap := make(map[string]string) + for _, h := range strings.Split(headers, ",") { + if parts := strings.SplitN(h, "=", 2); len(parts) == 2 { + headerMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + } + return headerMap +} diff --git a/internal/telemetry/tracing_test.go b/internal/telemetry/tracing_test.go new file mode 100644 index 00000000..f26f3bde --- /dev/null +++ b/internal/telemetry/tracing_test.go @@ -0,0 +1,374 @@ +package telemetry + +import ( + "context" + "os" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/trace/noop" +) + +// Test protocol constants for additional test scenarios +const ( + ProtocolInvalid = "invalid" +) + +// resetConfig is a helper to reset the singleton config for tests +func resetConfig() { + once = sync.Once{} + config = nil +} + +func TestSetupOTelSDK_Disabled(t *testing.T) { + resetConfig() + ctx := context.Background() + err := os.Setenv("OTEL_SDK_DISABLED", "true") + require.NoError(t, err) + defer func() { + _ = os.Unsetenv("OTEL_SDK_DISABLED") + }() + resetConfig() + + err = SetupOTelSDK(ctx) + require.NoError(t, err) + + // In a disabled state, the tracer provider should be a no-op provider + tp := otel.GetTracerProvider() + assert.IsType(t, noop.NewTracerProvider(), tp) + + // Shutdown should be a no-op function + assert.NoError(t, err) +} + +func TestSetupOTelSDKEnabled(t *testing.T) { + resetConfig() + ctx := context.Background() + err := os.Setenv(OtelSdkDisabled, "false") + require.NoError(t, err) + defer func() { + _ = os.Unsetenv(OtelSdkDisabled) + }() + + err = SetupOTelSDK(ctx) + require.NoError(t, err) +} + +func TestNewTracerProviderDevelopment(t *testing.T) { + resetConfig() + ctx := context.Background() + res := resource.NewSchemaless() + cfg := &Telemetry{ + Environment: "development", + } + exporter, _ := stdouttrace.New() + + tp, err := newTracerProvider(ctx, cfg, exporter, res) + require.NoError(t, err) + assert.NotNil(t, tp) +} + +func TestNewTracerProviderProduction(t *testing.T) { + resetConfig() + ctx := context.Background() + res := resource.NewSchemaless() + cfg := &Telemetry{ + Environment: "production", + SamplingRatio: 0.5, + } + exporter, _ := stdouttrace.New() + + tp, err := newTracerProvider(ctx, cfg, exporter, res) + require.NoError(t, err) + assert.NotNil(t, tp) +} + +func TestCreateExporterDevelopment(t *testing.T) { + resetConfig() + ctx := context.Background() + cfg := &Telemetry{ + Environment: "development", + } + + exporter, err := createExporter(ctx, cfg) + require.NoError(t, err) + assert.NotNil(t, exporter) + assert.IsType(t, &stdouttrace.Exporter{}, exporter) +} + +func TestCreateExporterNoEndpoint(t *testing.T) { + resetConfig() + ctx := context.Background() + cfg := &Telemetry{ + Environment: "production", + } + + exporter, err := createExporter(ctx, cfg) + require.NoError(t, err) + assert.NotNil(t, exporter) + assert.IsType(t, &stdouttrace.Exporter{}, exporter) +} + +func TestCreateExporterWithEndpoint(t *testing.T) { + resetConfig() + ctx := context.Background() + cfg := &Telemetry{ + Environment: "production", + Endpoint: "http://localhost:4317", + Protocol: ProtocolAuto, + } + + exporter, err := createExporter(ctx, cfg) + require.NoError(t, err) + assert.NotNil(t, exporter) +} + +func TestCreateExporterWithInsecure(t *testing.T) { + resetConfig() + ctx := context.Background() + cfg := &Telemetry{ + Environment: "production", + Endpoint: "localhost:4317", + Insecure: true, + } + + // This should not fail, as insecure is handled by the exporters + _, err := createExporter(ctx, cfg) + require.NoError(t, err) +} + +func TestCreateExporterWithAuthHeaders(t *testing.T) { + resetConfig() + ctx := context.Background() + cfg := &Telemetry{ + Environment: "production", + Endpoint: "http://localhost:4317", + Protocol: ProtocolAuto, + } + + // Set auth header + err := os.Setenv(OtelExporterOtlpHeaders, "Authorization=Bearer token123") + require.NoError(t, err) + defer func() { + _ = os.Unsetenv(OtelExporterOtlpHeaders) + }() + + exporter, err := createExporter(ctx, cfg) + require.NoError(t, err) + assert.NotNil(t, exporter) + + // Clean up + err = exporter.Shutdown(ctx) + assert.NoError(t, err) +} + +func TestSetupOTelSDKWithCancellation(t *testing.T) { + resetConfig() + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel context immediately + + err := SetupOTelSDK(ctx) + require.Error(t, err) // Expect an error due to context cancellation +} + +func TestProtocolDetection(t *testing.T) { + tests := []struct { + name string + endpoint string + expected string + }{ + {"gRPC port 4317", "localhost:4317", ProtocolGRPC}, + {"HTTP port 4318", "localhost:4318", ProtocolHTTP}, + {"gRPC port 4317 without scheme", "localhost:4317", ProtocolGRPC}, + {"HTTP port 4318 without scheme", "localhost:4318", ProtocolHTTP}, + {"gRPC with docker internal", "host.docker.internal:4317", ProtocolGRPC}, + {"HTTP with docker internal", "host.docker.internal:4318", ProtocolHTTP}, + {"No port specified", "localhost", ProtocolHTTP}, + {"Unknown port", "localhost:1234", ProtocolHTTP}, + {"HTTPS with gRPC port", "https://localhost:4317", ProtocolGRPC}, + {"HTTPS with HTTP port", "https://localhost:4318", ProtocolHTTP}, + {"gRPC with path", "localhost:4317/v1/traces", ProtocolGRPC}, + {"HTTP with path", "localhost:4318/v1/traces", ProtocolHTTP}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := detectProtocol(tt.endpoint) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestEndpointNormalization(t *testing.T) { + tests := []struct { + name string + endpoint string + expected string + }{ + {"Basic gRPC endpoint", "localhost:4317", "localhost:4317"}, + {"gRPC with path", "localhost:4317/v1/traces", "localhost:4317/v1/traces"}, + {"gRPC without scheme", "localhost:4317", "localhost:4317"}, + {"gRPC with HTTPS", "https://localhost:4317", "localhost:4317"}, + {"Docker internal gRPC", "host.docker.internal:4317", "host.docker.internal:4317"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := normalizeGRPCEndpoint(tt.endpoint) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestHTTPEndpointNormalization(t *testing.T) { + tests := []struct { + name string + endpoint string + insecure bool + expected string + }{ + {"Basic HTTP endpoint", "http://localhost:4318", false, "http://localhost:4318/v1/traces"}, + {"HTTP with path", "http://localhost:4318/v1/traces", false, "http://localhost:4318/v1/traces"}, + {"HTTP without scheme - secure localhost", "localhost:4318", false, "http://localhost:4318/v1/traces"}, + {"HTTP without scheme - insecure localhost", "localhost:4318", true, "http://localhost:4318/v1/traces"}, + {"HTTP with trailing slash", "http://localhost:4318/", false, "http://localhost:4318/v1/traces"}, + {"Docker internal HTTP - secure", "host.docker.internal:4318", false, "http://host.docker.internal:4318/v1/traces"}, + {"Docker internal HTTP - insecure", "host.docker.internal:4318", true, "http://host.docker.internal:4318/v1/traces"}, + {"Remote endpoint - secure", "collector.example.com:4318", false, "https://collector.example.com:4318/v1/traces"}, + {"Remote endpoint - insecure", "collector.example.com:4318", true, "http://collector.example.com:4318/v1/traces"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := normalizeHTTPEndpoint(tt.endpoint, tt.insecure) + assert.Equal(t, tt.expected, result, "HTTP endpoint normalization failed for: %s", tt.endpoint) + }) + } +} + +func TestParseHeaders(t *testing.T) { + tests := []struct { + name string + headers string + want map[string]string + }{ + {"Empty string", "", map[string]string{}}, + {"Single header", "key=value", map[string]string{"key": "value"}}, + {"Multiple headers", "key1=value1,key2=value2", map[string]string{"key1": "value1", "key2": "value2"}}, + {"Headers with spaces", " key1 = value1 , key2 = value2 ", map[string]string{"key1": "value1", "key2": "value2"}}, + {"Invalid header format", "key-value,key2", map[string]string{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseHeaders(tt.headers) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestCreateExporterWithProtocol(t *testing.T) { + + ctx := context.Background() + + tests := []struct { + name string + config *Telemetry + shouldError bool + description string + }{ + { + "gRPC protocol", + &Telemetry{ + Environment: "development", + Endpoint: "localhost:4317", + Protocol: ProtocolGRPC, + }, + false, + "Should create gRPC exporter", + }, + { + "HTTP protocol", + &Telemetry{ + Environment: "development", + Endpoint: "localhost:4318", + Protocol: ProtocolHTTP, + }, + false, + "Should create HTTP exporter", + }, + { + "Auto protocol with gRPC port", + &Telemetry{ + Environment: "development", + Endpoint: "localhost:4317", + Protocol: ProtocolAuto, + }, + false, + "Should auto-detect gRPC", + }, + { + "Auto protocol with HTTP port", + &Telemetry{ + Environment: "development", + Endpoint: "localhost:4318", + Protocol: ProtocolAuto, + }, + false, + "Should auto-detect HTTP", + }, + { + "gRPC protocol with insecure", + &Telemetry{ + Environment: "production", + Endpoint: "localhost:4317", + Protocol: ProtocolGRPC, + Insecure: true, + }, + false, + "Should create gRPC exporter with insecure", + }, + { + "HTTP protocol with insecure", + &Telemetry{ + Environment: "production", + Endpoint: "localhost:4318", + Protocol: ProtocolHTTP, + Insecure: true, + }, + false, + "Should create HTTP exporter with insecure", + }, + { + "Invalid protocol", + &Telemetry{ + Environment: "development", + Endpoint: "localhost:1234", + Protocol: ProtocolInvalid, + }, + true, + "Should return error for invalid protocol", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetConfig() + exporter, err := createExporter(ctx, tt.config) + if tt.shouldError { + require.Error(t, err, tt.description) + assert.Nil(t, exporter, tt.description) + } else { + require.NoError(t, err, tt.description) + assert.NotNil(t, exporter, tt.description) + err = exporter.Shutdown(ctx) + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/argo/argo.go b/pkg/argo/argo.go index 566a4a08..a24e9780 100644 --- a/pkg/argo/argo.go +++ b/pkg/argo/argo.go @@ -13,6 +13,8 @@ import ( "strings" "time" + "github.com/kagent-dev/tools/internal/commands" + "github.com/kagent-dev/tools/internal/telemetry" "github.com/kagent-dev/tools/pkg/utils" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -20,8 +22,6 @@ import ( // Argo Rollouts tools -var kubeConfig = "" - func handleVerifyArgoRolloutsControllerInstall(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { ns := mcp.ParseString(request, "namespace", "argo-rollouts") label := mcp.ParseString(request, "label", "app.kubernetes.io/component=rollouts-controller") @@ -76,10 +76,11 @@ func handleVerifyKubectlPluginInstall(ctx context.Context, request mcp.CallToolR } func runArgoRolloutCommand(ctx context.Context, args []string) (string, error) { - if kubeConfig != "" { - args = append(args, "--kubeconfig", kubeConfig) - } - return utils.RunCommandWithContext(ctx, "kubectl", args) + kubeconfigPath := utils.GetKubeconfig() + return commands.NewCommandBuilder("kubectl"). + WithArgs(args...). + WithKubeconfig(kubeconfigPath). + Execute(ctx) } func handlePromoteRollout(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -198,9 +199,13 @@ func getSystemArchitecture() (string, error) { } } -func getLatestVersion() string { +func getLatestVersion(ctx context.Context) string { client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Get("https://api.github.com/repos/argoproj-labs/rollouts-plugin-trafficrouter-gatewayapi/releases/latest") + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/repos/argoproj-labs/rollouts-plugin-trafficrouter-gatewayapi/releases/latest", nil) + if err != nil { + return "0.5.0" // Default version + } + resp, err := client.Do(req) if err != nil { return "0.5.0" // Default version } @@ -220,7 +225,7 @@ func getLatestVersion() string { return "0.5.0" } -func configureGatewayPlugin(version, namespace string) GatewayPluginStatus { +func configureGatewayPlugin(ctx context.Context, version, namespace string) GatewayPluginStatus { arch, err := getSystemArchitecture() if err != nil { return GatewayPluginStatus{ @@ -230,7 +235,7 @@ func configureGatewayPlugin(version, namespace string) GatewayPluginStatus { } if version == "" { - version = getLatestVersion() + version = getLatestVersion(ctx) } configMap := fmt.Sprintf(`apiVersion: v1 @@ -263,11 +268,12 @@ data: tmpFile.Close() // Apply the ConfigMap - _, err = utils.RunCommandWithContext(context.Background(), "kubectl", []string{"apply", "-f", tmpFile.Name()}) + cmdArgs := []string{"apply", "-f", tmpFile.Name()} + output, err := runArgoRolloutCommand(ctx, cmdArgs) if err != nil { return GatewayPluginStatus{ Installed: false, - ErrorMessage: fmt.Sprintf("Failed to configure Gateway API plugin: %s", err.Error()), + ErrorMessage: fmt.Sprintf("Error applying Gateway API plugin config: %s. Output: %s", err.Error(), output), } } @@ -304,7 +310,7 @@ func handleVerifyGatewayPlugin(ctx context.Context, request mcp.CallToolRequest) } // Configure plugin - status := configureGatewayPlugin(version, namespace) + status := configureGatewayPlugin(ctx, version, namespace) return mcp.NewToolResultText(status.String()), nil } @@ -348,48 +354,47 @@ func handleCheckPluginLogs(ctx context.Context, request mcp.CallToolRequest) (*m return mcp.NewToolResultText(status.String()), nil } -func RegisterArgoTools(s *server.MCPServer, kubeconfig string) { - kubeConfig = kubeconfig +func RegisterTools(s *server.MCPServer) { s.AddTool(mcp.NewTool("argo_verify_argo_rollouts_controller_install", mcp.WithDescription("Verify that the Argo Rollouts controller is installed and running"), mcp.WithString("namespace", mcp.Description("The namespace where Argo Rollouts is installed")), mcp.WithString("label", mcp.Description("The label of the Argo Rollouts controller pods")), - ), handleVerifyArgoRolloutsControllerInstall) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_verify_argo_rollouts_controller_install", handleVerifyArgoRolloutsControllerInstall))) s.AddTool(mcp.NewTool("argo_verify_kubectl_plugin_install", mcp.WithDescription("Verify that the kubectl Argo Rollouts plugin is installed"), - ), handleVerifyKubectlPluginInstall) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_verify_kubectl_plugin_install", handleVerifyKubectlPluginInstall))) s.AddTool(mcp.NewTool("argo_promote_rollout", mcp.WithDescription("Promote a paused rollout to the next step"), mcp.WithString("rollout_name", mcp.Description("The name of the rollout to promote"), mcp.Required()), mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), mcp.WithString("full", mcp.Description("Promote the rollout to the final step")), - ), handlePromoteRollout) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_promote_rollout", handlePromoteRollout))) s.AddTool(mcp.NewTool("argo_pause_rollout", mcp.WithDescription("Pause a rollout"), mcp.WithString("rollout_name", mcp.Description("The name of the rollout to pause"), mcp.Required()), mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), - ), handlePauseRollout) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_pause_rollout", handlePauseRollout))) s.AddTool(mcp.NewTool("argo_set_rollout_image", mcp.WithDescription("Set the image of a rollout"), mcp.WithString("rollout_name", mcp.Description("The name of the rollout to set the image for"), mcp.Required()), mcp.WithString("container_image", mcp.Description("The container image to set for the rollout"), mcp.Required()), mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), - ), handleSetRolloutImage) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_set_rollout_image", handleSetRolloutImage))) s.AddTool(mcp.NewTool("argo_verify_gateway_plugin", mcp.WithDescription("Verify the installation status of the Argo Rollouts Gateway API plugin"), mcp.WithString("version", mcp.Description("The version of the plugin to check")), mcp.WithString("namespace", mcp.Description("The namespace for the plugin resources")), mcp.WithString("should_install", mcp.Description("Whether to install the plugin if not found")), - ), handleVerifyGatewayPlugin) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_verify_gateway_plugin", handleVerifyGatewayPlugin))) s.AddTool(mcp.NewTool("argo_check_plugin_logs", mcp.WithDescription("Check the logs of the Argo Rollouts Gateway API plugin"), mcp.WithString("namespace", mcp.Description("The namespace of the plugin resources")), mcp.WithString("timeout", mcp.Description("Timeout for log collection in seconds")), - ), handleCheckPluginLogs) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_check_plugin_logs", handleCheckPluginLogs))) } diff --git a/pkg/argo/argo_test.go b/pkg/argo/argo_test.go index 4a80823c..0f90c39c 100644 --- a/pkg/argo/argo_test.go +++ b/pkg/argo/argo_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/kagent-dev/tools/pkg/utils" + "github.com/kagent-dev/tools/internal/cmd" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -27,11 +27,11 @@ func getResultText(result *mcp.CallToolResult) string { // Test Argo Rollouts Promote func TestHandlePromoteRollout(t *testing.T) { t.Run("promote rollout basic", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" promoted` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -52,15 +52,15 @@ func TestHandlePromoteRollout(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "promote", "myapp"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "promote", "myapp", "--timeout", "30s"}, callLog[0].Args) }) t.Run("promote rollout with namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" promoted` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "-n", "production", "myapp"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "-n", "production", "myapp", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -77,15 +77,15 @@ func TestHandlePromoteRollout(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "promote", "-n", "production", "myapp"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "promote", "-n", "production", "myapp", "--timeout", "30s"}, callLog[0].Args) }) t.Run("promote rollout with full flag", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" fully promoted` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp", "--full"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp", "--full", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -102,12 +102,12 @@ func TestHandlePromoteRollout(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "promote", "myapp", "--full"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "promote", "myapp", "--full", "--timeout", "30s"}, callLog[0].Args) }) t.Run("missing required parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -125,9 +125,9 @@ func TestHandlePromoteRollout(t *testing.T) { }) t.Run("kubectl command failure", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp"}, "", assert.AnError) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp", "--timeout", "30s"}, "", assert.AnError) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -145,11 +145,11 @@ func TestHandlePromoteRollout(t *testing.T) { // Test Argo Rollouts Pause func TestHandlePauseRollout(t *testing.T) { t.Run("pause rollout basic", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" paused` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "pause", "myapp"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "pause", "myapp", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -170,15 +170,15 @@ func TestHandlePauseRollout(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "pause", "myapp"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "pause", "myapp", "--timeout", "30s"}, callLog[0].Args) }) t.Run("pause rollout with namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" paused` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "pause", "-n", "production", "myapp"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "pause", "-n", "production", "myapp", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -195,12 +195,12 @@ func TestHandlePauseRollout(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "pause", "-n", "production", "myapp"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "pause", "-n", "production", "myapp", "--timeout", "30s"}, callLog[0].Args) }) t.Run("missing required parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -221,11 +221,11 @@ func TestHandlePauseRollout(t *testing.T) { // Test Argo Rollouts Set Image func TestHandleSetRolloutImage(t *testing.T) { t.Run("set rollout image basic", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" image updated` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "set", "image", "myapp", "nginx:latest"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "set", "image", "myapp", "nginx:latest", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -247,15 +247,15 @@ func TestHandleSetRolloutImage(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "set", "image", "myapp", "nginx:latest"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "set", "image", "myapp", "nginx:latest", "--timeout", "30s"}, callLog[0].Args) }) t.Run("set rollout image with namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" image updated` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "set", "image", "myapp", "nginx:1.20", "-n", "production"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "set", "image", "myapp", "nginx:1.20", "-n", "production", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -273,12 +273,12 @@ func TestHandleSetRolloutImage(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "set", "image", "myapp", "nginx:1.20", "-n", "production"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "set", "image", "myapp", "nginx:1.20", "-n", "production", "--timeout", "30s"}, callLog[0].Args) }) t.Run("missing rollout_name parameter", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -297,8 +297,8 @@ func TestHandleSetRolloutImage(t *testing.T) { }) t.Run("missing container_image parameter", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -334,7 +334,7 @@ func TestGetSystemArchitecture(t *testing.T) { } func TestGetLatestVersion(t *testing.T) { - version := getLatestVersion() + version := getLatestVersion(context.Background()) if version == "" { t.Error("Expected non-empty version") } @@ -367,11 +367,11 @@ func TestGatewayPluginStatus(t *testing.T) { // Test Verify Gateway Plugin func TestHandleVerifyGatewayPlugin(t *testing.T) { t.Run("verify gateway plugin without install", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `gateway-api-plugin not found` - mock.AddCommandString("kubectl", []string{"get", "configmap", "argo-rollouts-config", "-n", "argo-rollouts", "-o", "yaml"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"get", "configmap", "argo-rollouts-config", "-n", "argo-rollouts", "-o", "yaml", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -394,11 +394,11 @@ func TestHandleVerifyGatewayPlugin(t *testing.T) { }) t.Run("verify gateway plugin with custom namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `gateway-api-plugin-abc123` - mock.AddCommandString("kubectl", []string{"get", "configmap", "argo-rollouts-config", "-n", "custom-namespace", "-o", "yaml"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"get", "configmap", "argo-rollouts-config", "-n", "custom-namespace", "-o", "yaml", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -423,11 +423,11 @@ func TestHandleVerifyGatewayPlugin(t *testing.T) { // Test Verify Argo Rollouts Controller Install func TestHandleVerifyArgoRolloutsControllerInstall(t *testing.T) { t.Run("verify controller install", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `argo-rollouts-controller-manager-abc123` - mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app.kubernetes.io/name=argo-rollouts", "-n", "argo-rollouts", "-o", "jsonpath={.items[*].metadata.name}"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app.kubernetes.io/name=argo-rollouts", "-n", "argo-rollouts", "-o", "jsonpath={.items[*].metadata.name}", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} result, err := handleVerifyArgoRolloutsControllerInstall(ctx, request) @@ -444,11 +444,11 @@ func TestHandleVerifyArgoRolloutsControllerInstall(t *testing.T) { }) t.Run("verify controller install with custom namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `argo-rollouts-controller-manager-abc123` - mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app.kubernetes.io/name=argo-rollouts", "-n", "custom-argo", "-o", "jsonpath={.items[*].metadata.name}"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app.kubernetes.io/name=argo-rollouts", "-n", "custom-argo", "-o", "jsonpath={.items[*].metadata.name}", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -469,11 +469,11 @@ func TestHandleVerifyArgoRolloutsControllerInstall(t *testing.T) { }) t.Run("verify controller install with custom label", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `argo-rollouts-controller-manager-abc123` - mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app=custom-rollouts", "-n", "argo-rollouts", "-o", "jsonpath={.items[*].metadata.name}"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app=custom-rollouts", "-n", "argo-rollouts", "-o", "jsonpath={.items[*].metadata.name}", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -497,11 +497,11 @@ func TestHandleVerifyArgoRolloutsControllerInstall(t *testing.T) { // Test Verify Kubectl Plugin Install func TestHandleVerifyKubectlPluginInstall(t *testing.T) { t.Run("verify kubectl plugin install", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `kubectl-argo-rollouts` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "version"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "version", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} result, err := handleVerifyKubectlPluginInstall(ctx, request) @@ -513,13 +513,13 @@ func TestHandleVerifyKubectlPluginInstall(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "version"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "version", "--timeout", "30s"}, callLog[0].Args) }) t.Run("kubectl plugin command failure", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("kubectl", []string{"plugin", "list"}, "", assert.AnError) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("kubectl", []string{"plugin", "list", "--timeout", "30s"}, "", assert.AnError) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} result, err := handleVerifyKubectlPluginInstall(ctx, request) diff --git a/pkg/cilium/cilium.go b/pkg/cilium/cilium.go index a84cae32..6ad576cd 100644 --- a/pkg/cilium/cilium.go +++ b/pkg/cilium/cilium.go @@ -3,21 +3,21 @@ package cilium import ( "context" "fmt" - "strings" + "github.com/kagent-dev/tools/internal/commands" + "github.com/kagent-dev/tools/internal/telemetry" "github.com/kagent-dev/tools/pkg/utils" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) -var kubeConfig = "" - func runCiliumCliWithContext(ctx context.Context, args ...string) (string, error) { - if kubeConfig != "" { - args = append([]string{"--kubeconfig", kubeConfig}, args...) - } - return utils.RunCommandWithContext(ctx, "cilium", args) + kubeconfigPath := utils.GetKubeconfig() + return commands.NewCommandBuilder("cilium"). + WithArgs(args...). + WithKubeconfig(kubeconfigPath). + Execute(ctx) } func handleCiliumStatusAndVersion(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -200,70 +200,66 @@ func handleToggleClusterMesh(ctx context.Context, request mcp.CallToolRequest) ( return mcp.NewToolResultText(output), nil } -func RegisterCiliumTools(s *server.MCPServer, kubeconfig string) { - kubeConfig = kubeconfig +func RegisterTools(s *server.MCPServer) { - // Register debug tools - RegisterCiliumDbgTools(s) - - // Register main Cilium tools + // Register all Cilium tools (main and debug) s.AddTool(mcp.NewTool("cilium_status_and_version", mcp.WithDescription("Get the status and version of Cilium installation"), - ), handleCiliumStatusAndVersion) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_status_and_version", handleCiliumStatusAndVersion))) s.AddTool(mcp.NewTool("cilium_upgrade_cilium", mcp.WithDescription("Upgrade Cilium on the cluster"), mcp.WithString("cluster_name", mcp.Description("The name of the cluster to upgrade Cilium on")), mcp.WithString("datapath_mode", mcp.Description("The datapath mode to use for Cilium (tunnel, native, aws-eni, gke, azure, aks-byocni)")), - ), handleUpgradeCilium) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_upgrade_cilium", handleUpgradeCilium))) s.AddTool(mcp.NewTool("cilium_install_cilium", mcp.WithDescription("Install Cilium on the cluster"), mcp.WithString("cluster_name", mcp.Description("The name of the cluster to install Cilium on")), mcp.WithString("cluster_id", mcp.Description("The ID of the cluster to install Cilium on")), mcp.WithString("datapath_mode", mcp.Description("The datapath mode to use for Cilium (tunnel, native, aws-eni, gke, azure, aks-byocni)")), - ), handleInstallCilium) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_install_cilium", handleInstallCilium))) s.AddTool(mcp.NewTool("cilium_uninstall_cilium", mcp.WithDescription("Uninstall Cilium from the cluster"), - ), handleUninstallCilium) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_uninstall_cilium", handleUninstallCilium))) s.AddTool(mcp.NewTool("cilium_connect_to_remote_cluster", mcp.WithDescription("Connect to a remote cluster for cluster mesh"), mcp.WithString("cluster_name", mcp.Description("The name of the destination cluster"), mcp.Required()), mcp.WithString("context", mcp.Description("The kubectl context for the destination cluster")), - ), handleConnectToRemoteCluster) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_connect_to_remote_cluster", handleConnectToRemoteCluster))) s.AddTool(mcp.NewTool("cilium_disconnect_remote_cluster", mcp.WithDescription("Disconnect from a remote cluster"), mcp.WithString("cluster_name", mcp.Description("The name of the destination cluster"), mcp.Required()), - ), handleDisconnectRemoteCluster) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_disconnect_remote_cluster", handleDisconnectRemoteCluster))) s.AddTool(mcp.NewTool("cilium_list_bgp_peers", mcp.WithDescription("List BGP peers"), - ), handleListBGPPeers) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_bgp_peers", handleListBGPPeers))) s.AddTool(mcp.NewTool("cilium_list_bgp_routes", mcp.WithDescription("List BGP routes"), - ), handleListBGPRoutes) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_bgp_routes", handleListBGPRoutes))) s.AddTool(mcp.NewTool("cilium_show_cluster_mesh_status", mcp.WithDescription("Show cluster mesh status"), - ), handleShowClusterMeshStatus) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_cluster_mesh_status", handleShowClusterMeshStatus))) s.AddTool(mcp.NewTool("cilium_show_features_status", mcp.WithDescription("Show Cilium features status"), - ), handleShowFeaturesStatus) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_features_status", handleShowFeaturesStatus))) s.AddTool(mcp.NewTool("cilium_toggle_hubble", mcp.WithDescription("Enable or disable Hubble"), mcp.WithString("enable", mcp.Description("Set to 'true' to enable, 'false' to disable")), - ), handleToggleHubble) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_hubble", handleToggleHubble))) s.AddTool(mcp.NewTool("cilium_toggle_cluster_mesh", mcp.WithDescription("Enable or disable cluster mesh"), mcp.WithString("enable", mcp.Description("Set to 'true' to enable, 'false' to disable")), - ), handleToggleClusterMesh) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_cluster_mesh", handleToggleClusterMesh))) // Add tools that are also needed by cilium-manager agent s.AddTool(mcp.NewTool("cilium_get_daemon_status", @@ -276,12 +272,12 @@ func RegisterCiliumTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("show_all_redirects", mcp.Description("Whether to show all redirects")), mcp.WithString("brief", mcp.Description("Whether to show a brief status")), mcp.WithString("node_name", mcp.Description("The name of the node to get the daemon status for")), - ), handleGetDaemonStatus) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_daemon_status", handleGetDaemonStatus))) s.AddTool(mcp.NewTool("cilium_get_endpoints_list", mcp.WithDescription("Get the list of all endpoints in the cluster"), mcp.WithString("node_name", mcp.Description("The name of the node to get the endpoints list for")), - ), handleGetEndpointsList) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_endpoints_list", handleGetEndpointsList))) s.AddTool(mcp.NewTool("cilium_get_endpoint_details", mcp.WithDescription("List the details of an endpoint in the cluster"), @@ -289,7 +285,7 @@ func RegisterCiliumTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("labels", mcp.Description("The labels of the endpoint to get details for")), mcp.WithString("output_format", mcp.Description("The output format of the endpoint details (json, yaml, jsonpath)")), mcp.WithString("node_name", mcp.Description("The name of the node to get the endpoint details for")), - ), handleGetEndpointDetails) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_endpoint_details", handleGetEndpointDetails))) s.AddTool(mcp.NewTool("cilium_show_configuration_options", mcp.WithDescription("Show Cilium configuration options"), @@ -297,26 +293,26 @@ func RegisterCiliumTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("list_read_only", mcp.Description("Whether to list read-only configuration options")), mcp.WithString("list_options", mcp.Description("Whether to list options")), mcp.WithString("node_name", mcp.Description("The name of the node to show the configuration options for")), - ), handleShowConfigurationOptions) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_configuration_options", handleShowConfigurationOptions))) s.AddTool(mcp.NewTool("cilium_toggle_configuration_option", mcp.WithDescription("Toggle a Cilium configuration option"), mcp.WithString("option", mcp.Description("The option to toggle"), mcp.Required()), mcp.WithString("value", mcp.Description("The value to set the option to (true/false)"), mcp.Required()), mcp.WithString("node_name", mcp.Description("The name of the node to toggle the configuration option for")), - ), handleToggleConfigurationOption) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_configuration_option", handleToggleConfigurationOption))) s.AddTool(mcp.NewTool("cilium_list_services", mcp.WithDescription("List services for the cluster"), mcp.WithString("show_cluster_mesh_affinity", mcp.Description("Whether to show cluster mesh affinity")), mcp.WithString("node_name", mcp.Description("The name of the node to get the services for")), - ), handleListServices) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_services", handleListServices))) s.AddTool(mcp.NewTool("cilium_get_service_information", mcp.WithDescription("Get information about a service in the cluster"), mcp.WithString("service_id", mcp.Description("The ID of the service to get information about"), mcp.Required()), mcp.WithString("node_name", mcp.Description("The name of the node to get the service information for")), - ), handleGetServiceInformation) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_service_information", handleGetServiceInformation))) s.AddTool(mcp.NewTool("cilium_update_service", mcp.WithDescription("Update a service in the cluster"), @@ -335,35 +331,258 @@ func RegisterCiliumTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("protocol", mcp.Description("The protocol to update the service with")), mcp.WithString("states", mcp.Description("The states to update the service with")), mcp.WithString("node_name", mcp.Description("The name of the node to update the service on")), - ), handleUpdateService) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_service", handleUpdateService))) s.AddTool(mcp.NewTool("cilium_delete_service", mcp.WithDescription("Delete a service from the cluster"), mcp.WithString("service_id", mcp.Description("The ID of the service to delete")), mcp.WithString("all", mcp.Description("Whether to delete all services (true/false)")), mcp.WithString("node_name", mcp.Description("The name of the node to delete the service from")), - ), handleDeleteService) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_service", handleDeleteService))) + + // Debug tools (previously in RegisterCiliumDbgTools) + s.AddTool(mcp.NewTool("cilium_get_endpoint_details", + mcp.WithDescription("List the details of an endpoint in the cluster"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to get details for")), + mcp.WithString("labels", mcp.Description("The labels of the endpoint to get details for")), + mcp.WithString("output_format", mcp.Description("The output format of the endpoint details (json, yaml, jsonpath)")), + mcp.WithString("node_name", mcp.Description("The name of the node to get the endpoint details for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_endpoint_details", handleGetEndpointDetails))) + + s.AddTool(mcp.NewTool("cilium_get_endpoint_logs", + mcp.WithDescription("Get the logs of an endpoint in the cluster"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to get logs for"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to get the endpoint logs for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_endpoint_logs", handleGetEndpointLogs))) + + s.AddTool(mcp.NewTool("cilium_get_endpoint_health", + mcp.WithDescription("Get the health of an endpoint in the cluster"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to get health for"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to get the endpoint health for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_endpoint_health", handleGetEndpointHealth))) + + s.AddTool(mcp.NewTool("cilium_manage_endpoint_labels", + mcp.WithDescription("Manage the labels (add or delete) of an endpoint in the cluster"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage labels for"), mcp.Required()), + mcp.WithString("labels", mcp.Description("Space-separated labels to manage (e.g., 'key1=value1 key2=value2')"), mcp.Required()), + mcp.WithString("action", mcp.Description("The action to perform on the labels (add or delete)"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint labels on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_manage_endpoint_labels", handleManageEndpointLabels))) + + s.AddTool(mcp.NewTool("cilium_manage_endpoint_config", + mcp.WithDescription("Manage the configuration of an endpoint in the cluster"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage configuration for"), mcp.Required()), + mcp.WithString("config", mcp.Description("The configuration to manage for the endpoint provided as a space-separated list of key-value pairs (e.g. 'DropNotification=false TraceNotification=false')"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint configuration on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_manage_endpoint_config", handleManageEndpointConfiguration))) + + s.AddTool(mcp.NewTool("cilium_disconnect_endpoint", + mcp.WithDescription("Disconnect an endpoint from the network"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to disconnect"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to disconnect the endpoint from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_disconnect_endpoint", handleDisconnectEndpoint))) + + s.AddTool(mcp.NewTool("cilium_list_identities", + mcp.WithDescription("List all identities in the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to list the identities for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_identities", handleListIdentities))) + + s.AddTool(mcp.NewTool("cilium_get_identity_details", + mcp.WithDescription("Get the details of an identity in the cluster"), + mcp.WithString("identity_id", mcp.Description("The ID of the identity to get details for"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to get the identity details for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_identity_details", handleGetIdentityDetails))) + + s.AddTool(mcp.NewTool("cilium_request_debugging_information", + mcp.WithDescription("Request debugging information for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the debugging information for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_request_debugging_information", handleRequestDebuggingInformation))) + + s.AddTool(mcp.NewTool("cilium_display_encryption_state", + mcp.WithDescription("Display the encryption state for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the encryption state for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_display_encryption_state", handleDisplayEncryptionState))) + + s.AddTool(mcp.NewTool("cilium_flush_ipsec_state", + mcp.WithDescription("Flush the IPsec state for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to flush the IPsec state for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_flush_ipsec_state", handleFlushIPsecState))) + + s.AddTool(mcp.NewTool("cilium_list_envoy_config", + mcp.WithDescription("List the Envoy configuration for a resource in the cluster"), + mcp.WithString("resource_name", mcp.Description("The name of the resource to get the Envoy configuration for"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to get the Envoy configuration for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_envoy_config", handleListEnvoyConfig))) + + s.AddTool(mcp.NewTool("cilium_fqdn_cache", + mcp.WithDescription("Manage the FQDN cache for the cluster"), + mcp.WithString("command", mcp.Description("The command to perform on the FQDN cache (list, clean, or a specific command)"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to manage the FQDN cache for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_fqdn_cache", handleFQDNCache))) + + s.AddTool(mcp.NewTool("cilium_show_dns_names", + mcp.WithDescription("Show the DNS names for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the DNS names for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_dns_names", handleShowDNSNames))) + + s.AddTool(mcp.NewTool("cilium_list_ip_addresses", + mcp.WithDescription("List the IP addresses for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the IP addresses for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_ip_addresses", handleListIPAddresses))) + + s.AddTool(mcp.NewTool("cilium_show_ip_cache_information", + mcp.WithDescription("Show the IP cache information for the cluster"), + mcp.WithString("cidr", mcp.Description("The CIDR of the IP to get cache information for")), + mcp.WithString("labels", mcp.Description("The labels of the IP to get cache information for")), + mcp.WithString("node_name", mcp.Description("The name of the node to get the IP cache information for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_ip_cache_information", handleShowIPCacheInformation))) + + s.AddTool(mcp.NewTool("cilium_delete_key_from_kv_store", + mcp.WithDescription("Delete a key from the kvstore for the cluster"), + mcp.WithString("key", mcp.Description("The key to delete from the kvstore"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the key from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_key_from_kv_store", handleDeleteKeyFromKVStore))) + + s.AddTool(mcp.NewTool("cilium_get_kv_store_key", + mcp.WithDescription("Get a key from the kvstore for the cluster"), + mcp.WithString("key", mcp.Description("The key to get from the kvstore"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to get the key from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_kv_store_key", handleGetKVStoreKey))) + + s.AddTool(mcp.NewTool("cilium_set_kv_store_key", + mcp.WithDescription("Set a key in the kvstore for the cluster"), + mcp.WithString("key", mcp.Description("The key to set in the kvstore"), mcp.Required()), + mcp.WithString("value", mcp.Description("The value to set in the kvstore"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to set the key in")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_set_kv_store_key", handleSetKVStoreKey))) + + s.AddTool(mcp.NewTool("cilium_show_load_information", + mcp.WithDescription("Show load information for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the load information for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_load_information", handleShowLoadInformation))) + + s.AddTool(mcp.NewTool("cilium_list_local_redirect_policies", + mcp.WithDescription("List local redirect policies for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the local redirect policies for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_local_redirect_policies", handleListLocalRedirectPolicies))) + + s.AddTool(mcp.NewTool("cilium_list_bpf_map_events", + mcp.WithDescription("List BPF map events for the cluster"), + mcp.WithString("map_name", mcp.Description("The name of the BPF map to get events for"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to get the BPF map events for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_bpf_map_events", handleListBPFMapEvents))) + + s.AddTool(mcp.NewTool("cilium_get_bpf_map", + mcp.WithDescription("Get BPF map for the cluster"), + mcp.WithString("map_name", mcp.Description("The name of the BPF map to get"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to get the BPF map for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_bpf_map", handleGetBPFMap))) + + s.AddTool(mcp.NewTool("cilium_list_bpf_maps", + mcp.WithDescription("List BPF maps for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the BPF maps for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_bpf_maps", handleListBPFMaps))) + + s.AddTool(mcp.NewTool("cilium_list_metrics", + mcp.WithDescription("List metrics for the cluster"), + mcp.WithString("match_pattern", mcp.Description("The match pattern to filter metrics by")), + mcp.WithString("node_name", mcp.Description("The name of the node to get the metrics for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_metrics", handleListMetrics))) + + s.AddTool(mcp.NewTool("cilium_list_cluster_nodes", + mcp.WithDescription("List cluster nodes for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the cluster nodes for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_cluster_nodes", handleListClusterNodes))) + + s.AddTool(mcp.NewTool("cilium_list_node_ids", + mcp.WithDescription("List node IDs for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the node IDs for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_node_ids", handleListNodeIds))) + + s.AddTool(mcp.NewTool("cilium_display_policy_node_information", + mcp.WithDescription("Display policy node information for the cluster"), + mcp.WithString("labels", mcp.Description("The labels to get policy node information for")), + mcp.WithString("node_name", mcp.Description("The name of the node to get policy node information for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_display_policy_node_information", handleDisplayPolicyNodeInformation))) + + s.AddTool(mcp.NewTool("cilium_delete_policy_rules", + mcp.WithDescription("Delete policy rules for the cluster"), + mcp.WithString("labels", mcp.Description("The labels to delete policy rules for")), + mcp.WithString("all", mcp.Description("Whether to delete all policy rules")), + mcp.WithString("node_name", mcp.Description("The name of the node to delete policy rules for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_policy_rules", handleDeletePolicyRules))) + + s.AddTool(mcp.NewTool("cilium_display_selectors", + mcp.WithDescription("Display selectors for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get selectors for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_display_selectors", handleDisplaySelectors))) + + s.AddTool(mcp.NewTool("cilium_list_xdp_cidr_filters", + mcp.WithDescription("List XDP CIDR filters for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the XDP CIDR filters for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_xdp_cidr_filters", handleListXDPCIDRFilters))) + + s.AddTool(mcp.NewTool("cilium_update_xdp_cidr_filters", + mcp.WithDescription("Update XDP CIDR filters for the cluster"), + mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to update the XDP filters for"), mcp.Required()), + mcp.WithString("revision", mcp.Description("The revision of the XDP filters to update")), + mcp.WithString("node_name", mcp.Description("The name of the node to update the XDP filters for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_xdp_cidr_filters", handleUpdateXDPCIDRFilters))) + + s.AddTool(mcp.NewTool("cilium_delete_xdp_cidr_filters", + mcp.WithDescription("Delete XDP CIDR filters for the cluster"), + mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to delete the XDP filters for"), mcp.Required()), + mcp.WithString("revision", mcp.Description("The revision of the XDP filters to delete")), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the XDP filters for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_xdp_cidr_filters", handleDeleteXDPCIDRFilters))) + + s.AddTool(mcp.NewTool("cilium_validate_cilium_network_policies", + mcp.WithDescription("Validate Cilium network policies for the cluster"), + mcp.WithString("enable_k8s", mcp.Description("Whether to enable k8s API discovery")), + mcp.WithString("enable_k8s_api_discovery", mcp.Description("Whether to enable k8s API discovery")), + mcp.WithString("node_name", mcp.Description("The name of the node to validate the Cilium network policies for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_validate_cilium_network_policies", handleValidateCiliumNetworkPolicies))) + + s.AddTool(mcp.NewTool("cilium_list_pcap_recorders", + mcp.WithDescription("List PCAP recorders for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to get the PCAP recorders for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_pcap_recorders", handleListPCAPRecorders))) + + s.AddTool(mcp.NewTool("cilium_get_pcap_recorder", + mcp.WithDescription("Get a PCAP recorder for the cluster"), + mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to get"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to get the PCAP recorder for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_pcap_recorder", handleGetPCAPRecorder))) + + s.AddTool(mcp.NewTool("cilium_delete_pcap_recorder", + mcp.WithDescription("Delete a PCAP recorder for the cluster"), + mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to delete"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the PCAP recorder from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_pcap_recorder", handleDeletePCAPRecorder))) + + s.AddTool(mcp.NewTool("cilium_update_pcap_recorder", + mcp.WithDescription("Update a PCAP recorder for the cluster"), + mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to update"), mcp.Required()), + mcp.WithString("filters", mcp.Description("The filters to update the PCAP recorder with"), mcp.Required()), + mcp.WithString("caplen", mcp.Description("The caplen to update the PCAP recorder with")), + mcp.WithString("id", mcp.Description("The id to update the PCAP recorder with")), + mcp.WithString("node_name", mcp.Description("The name of the node to update the PCAP recorder on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_pcap_recorder", handleUpdatePCAPRecorder))) } // -- Debug Tools -- func getCiliumPodNameWithContext(ctx context.Context, nodeName string) (string, error) { - args := []string{"get", "pod", "-l", "k8s-app=cilium", "-o", "name", "-n", "kube-system"} - if nodeName != "" { - args = append(args, "--field-selector", "spec.nodeName="+nodeName) - } - podName, err := utils.RunCommandWithContext(ctx, "kubectl", args) - if err != nil { - return "", fmt.Errorf("failed to get cilium pod name: %v", err) - } - if podName == "" { - return "", fmt.Errorf("no cilium pod found") - } - return strings.TrimSpace(podName), nil + args := []string{"get", "pods", "-n", "kube-system", "--selector=k8s-app=cilium", fmt.Sprintf("--field-selector=spec.nodeName=%s", nodeName), "-o", "jsonpath={.items[0].metadata.name}"} + kubeconfigPath := utils.GetKubeconfig() + return commands.NewCommandBuilder("kubectl"). + WithArgs(args...). + WithKubeconfig(kubeconfigPath). + Execute(ctx) } -func runCiliumDbgCommand(command, nodeName string) (string, error) { - return runCiliumDbgCommandWithContext(context.Background(), command, nodeName) +func runCiliumDbgCommand(ctx context.Context, command, nodeName string) (string, error) { + return runCiliumDbgCommandWithContext(ctx, command, nodeName) } func runCiliumDbgCommandWithContext(ctx context.Context, command, nodeName string) (string, error) { @@ -371,10 +590,12 @@ func runCiliumDbgCommandWithContext(ctx context.Context, command, nodeName strin if err != nil { return "", err } - cmdParts := strings.Fields(command) - args := []string{"exec", "-it", podName, "-n", "kube-system", "--", "cilium-dbg"} - args = append(args, cmdParts...) - return utils.RunCommandWithContext(ctx, "kubectl", args) + args := []string{"exec", "-it", podName, "--", "cilium-dbg", command} + kubeconfigPath := utils.GetKubeconfig() + return commands.NewCommandBuilder("kubectl"). + WithArgs(args...). + WithKubeconfig(kubeconfigPath). + Execute(ctx) } func handleGetEndpointDetails(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -392,7 +613,7 @@ func handleGetEndpointDetails(ctx context.Context, request mcp.CallToolRequest) return mcp.NewToolResultError("either endpoint_id or labels must be provided"), nil } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get endpoint details: %v", err)), nil } @@ -408,7 +629,7 @@ func handleGetEndpointLogs(ctx context.Context, request mcp.CallToolRequest) (*m } cmd := fmt.Sprintf("endpoint logs %s", endpointID) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get endpoint logs: %v", err)), nil } @@ -424,7 +645,7 @@ func handleGetEndpointHealth(ctx context.Context, request mcp.CallToolRequest) ( } cmd := fmt.Sprintf("endpoint health %s", endpointID) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get endpoint health: %v", err)), nil } @@ -442,7 +663,7 @@ func handleManageEndpointLabels(ctx context.Context, request mcp.CallToolRequest } cmd := fmt.Sprintf("endpoint labels %s --%s %s", endpointID, action, labels) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to manage endpoint labels: %v", err)), nil } @@ -462,7 +683,7 @@ func handleManageEndpointConfiguration(ctx context.Context, request mcp.CallTool } command := fmt.Sprintf("endpoint config %s %s", endpointID, config) - output, err := runCiliumDbgCommand(command, nodeName) + output, err := runCiliumDbgCommand(ctx, command, nodeName) if err != nil { return mcp.NewToolResultError("Error managing endpoint configuration: " + err.Error()), nil } @@ -479,7 +700,7 @@ func handleDisconnectEndpoint(ctx context.Context, request mcp.CallToolRequest) } cmd := fmt.Sprintf("endpoint disconnect %s", endpointID) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to disconnect endpoint: %v", err)), nil } @@ -489,7 +710,7 @@ func handleDisconnectEndpoint(ctx context.Context, request mcp.CallToolRequest) func handleGetEndpointsList(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("endpoint list", nodeName) + output, err := runCiliumDbgCommand(ctx, "endpoint list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get endpoints list: %v", err)), nil } @@ -499,7 +720,7 @@ func handleGetEndpointsList(ctx context.Context, request mcp.CallToolRequest) (* func handleListIdentities(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("identity list", nodeName) + output, err := runCiliumDbgCommand(ctx, "identity list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list identities: %v", err)), nil } @@ -515,7 +736,7 @@ func handleGetIdentityDetails(ctx context.Context, request mcp.CallToolRequest) } cmd := fmt.Sprintf("identity get %s", identityID) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get identity details: %v", err)), nil } @@ -539,7 +760,7 @@ func handleShowConfigurationOptions(ctx context.Context, request mcp.CallToolReq cmd = "endpoint config" } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to show configuration options: %v", err)), nil } @@ -561,7 +782,7 @@ func handleToggleConfigurationOption(ctx context.Context, request mcp.CallToolRe } cmd := fmt.Sprintf("endpoint config %s=%s", option, valueStr) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to toggle configuration option: %v", err)), nil } @@ -571,7 +792,7 @@ func handleToggleConfigurationOption(ctx context.Context, request mcp.CallToolRe func handleRequestDebuggingInformation(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("debuginfo", nodeName) + output, err := runCiliumDbgCommand(ctx, "debuginfo", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to request debugging information: %v", err)), nil } @@ -581,7 +802,7 @@ func handleRequestDebuggingInformation(ctx context.Context, request mcp.CallTool func handleDisplayEncryptionState(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("encrypt status", nodeName) + output, err := runCiliumDbgCommand(ctx, "encrypt status", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to display encryption state: %v", err)), nil } @@ -591,7 +812,7 @@ func handleDisplayEncryptionState(ctx context.Context, request mcp.CallToolReque func handleFlushIPsecState(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("encrypt flush -f", nodeName) + output, err := runCiliumDbgCommand(ctx, "encrypt flush -f", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to flush IPsec state: %v", err)), nil } @@ -607,7 +828,7 @@ func handleListEnvoyConfig(ctx context.Context, request mcp.CallToolRequest) (*m } cmd := fmt.Sprintf("envoy admin %s", resourceName) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list Envoy config: %v", err)), nil } @@ -620,12 +841,12 @@ func handleFQDNCache(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal var cmd string if command == "clean" { - cmd = "fqdn cache clean -f" + cmd = "fqdn cache clean" } else { - cmd = fmt.Sprintf("fqdn cache %s", command) + cmd = "fqdn cache list" } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to manage FQDN cache: %v", err)), nil } @@ -635,7 +856,7 @@ func handleFQDNCache(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal func handleShowDNSNames(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("dns names", nodeName) + output, err := runCiliumDbgCommand(ctx, "dns names", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to show DNS names: %v", err)), nil } @@ -645,7 +866,7 @@ func handleShowDNSNames(ctx context.Context, request mcp.CallToolRequest) (*mcp. func handleListIPAddresses(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("ip list", nodeName) + output, err := runCiliumDbgCommand(ctx, "ip list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list IP addresses: %v", err)), nil } @@ -666,7 +887,7 @@ func handleShowIPCacheInformation(ctx context.Context, request mcp.CallToolReque return mcp.NewToolResultError("either cidr or labels must be provided"), nil } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to show IP cache information: %v", err)), nil } @@ -682,7 +903,7 @@ func handleDeleteKeyFromKVStore(ctx context.Context, request mcp.CallToolRequest } cmd := fmt.Sprintf("kvstore delete %s", key) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to delete key from kvstore: %v", err)), nil } @@ -698,7 +919,7 @@ func handleGetKVStoreKey(ctx context.Context, request mcp.CallToolRequest) (*mcp } cmd := fmt.Sprintf("kvstore get %s", key) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get key from kvstore: %v", err)), nil } @@ -715,7 +936,7 @@ func handleSetKVStoreKey(ctx context.Context, request mcp.CallToolRequest) (*mcp } cmd := fmt.Sprintf("kvstore set %s=%s", key, value) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to set key in kvstore: %v", err)), nil } @@ -725,7 +946,7 @@ func handleSetKVStoreKey(ctx context.Context, request mcp.CallToolRequest) (*mcp func handleShowLoadInformation(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("loadinfo", nodeName) + output, err := runCiliumDbgCommand(ctx, "loadinfo", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to show load information: %v", err)), nil } @@ -735,7 +956,7 @@ func handleShowLoadInformation(ctx context.Context, request mcp.CallToolRequest) func handleListLocalRedirectPolicies(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("lrp list", nodeName) + output, err := runCiliumDbgCommand(ctx, "lrp list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list local redirect policies: %v", err)), nil } @@ -751,7 +972,7 @@ func handleListBPFMapEvents(ctx context.Context, request mcp.CallToolRequest) (* } cmd := fmt.Sprintf("bpf map events %s", mapName) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list BPF map events: %v", err)), nil } @@ -767,7 +988,7 @@ func handleGetBPFMap(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal } cmd := fmt.Sprintf("bpf map get %s", mapName) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get BPF map: %v", err)), nil } @@ -777,7 +998,7 @@ func handleGetBPFMap(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal func handleListBPFMaps(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("bpf map list", nodeName) + output, err := runCiliumDbgCommand(ctx, "bpf map list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list BPF maps: %v", err)), nil } @@ -795,7 +1016,7 @@ func handleListMetrics(ctx context.Context, request mcp.CallToolRequest) (*mcp.C cmd = "metrics list" } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list metrics: %v", err)), nil } @@ -805,7 +1026,7 @@ func handleListMetrics(ctx context.Context, request mcp.CallToolRequest) (*mcp.C func handleListClusterNodes(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("nodes list", nodeName) + output, err := runCiliumDbgCommand(ctx, "nodes list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list cluster nodes: %v", err)), nil } @@ -815,7 +1036,7 @@ func handleListClusterNodes(ctx context.Context, request mcp.CallToolRequest) (* func handleListNodeIds(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("nodeid list", nodeName) + output, err := runCiliumDbgCommand(ctx, "nodeid list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list node IDs: %v", err)), nil } @@ -833,7 +1054,7 @@ func handleDisplayPolicyNodeInformation(ctx context.Context, request mcp.CallToo cmd = "policy get" } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to display policy node information: %v", err)), nil } @@ -854,7 +1075,7 @@ func handleDeletePolicyRules(ctx context.Context, request mcp.CallToolRequest) ( return mcp.NewToolResultError("either labels or all=true must be provided"), nil } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to delete policy rules: %v", err)), nil } @@ -864,7 +1085,7 @@ func handleDeletePolicyRules(ctx context.Context, request mcp.CallToolRequest) ( func handleDisplaySelectors(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("policy selectors", nodeName) + output, err := runCiliumDbgCommand(ctx, "policy selectors", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to display selectors: %v", err)), nil } @@ -874,7 +1095,7 @@ func handleDisplaySelectors(ctx context.Context, request mcp.CallToolRequest) (* func handleListXDPCIDRFilters(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("prefilter list", nodeName) + output, err := runCiliumDbgCommand(ctx, "prefilter list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list XDP CIDR filters: %v", err)), nil } @@ -897,7 +1118,7 @@ func handleUpdateXDPCIDRFilters(ctx context.Context, request mcp.CallToolRequest cmd = fmt.Sprintf("prefilter update --cidr %s", cidrPrefixes) } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to update XDP CIDR filters: %v", err)), nil } @@ -920,7 +1141,7 @@ func handleDeleteXDPCIDRFilters(ctx context.Context, request mcp.CallToolRequest cmd = fmt.Sprintf("prefilter delete --cidr %s", cidrPrefixes) } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to delete XDP CIDR filters: %v", err)), nil } @@ -940,7 +1161,7 @@ func handleValidateCiliumNetworkPolicies(ctx context.Context, request mcp.CallTo cmd += " --enable-k8s-api-discovery" } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to validate Cilium network policies: %v", err)), nil } @@ -950,7 +1171,7 @@ func handleValidateCiliumNetworkPolicies(ctx context.Context, request mcp.CallTo func handleListPCAPRecorders(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand("recorder list", nodeName) + output, err := runCiliumDbgCommand(ctx, "recorder list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list PCAP recorders: %v", err)), nil } @@ -966,7 +1187,7 @@ func handleGetPCAPRecorder(ctx context.Context, request mcp.CallToolRequest) (*m } cmd := fmt.Sprintf("recorder get %s", recorderID) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get PCAP recorder: %v", err)), nil } @@ -982,7 +1203,7 @@ func handleDeletePCAPRecorder(ctx context.Context, request mcp.CallToolRequest) } cmd := fmt.Sprintf("recorder delete %s", recorderID) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to delete PCAP recorder: %v", err)), nil } @@ -1001,7 +1222,7 @@ func handleUpdatePCAPRecorder(ctx context.Context, request mcp.CallToolRequest) } cmd := fmt.Sprintf("recorder update %s --filters %s --caplen %s --id %s", recorderID, filters, caplen, id) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to update PCAP recorder: %v", err)), nil } @@ -1019,7 +1240,7 @@ func handleListServices(ctx context.Context, request mcp.CallToolRequest) (*mcp. cmd = "service list" } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list services: %v", err)), nil } @@ -1035,7 +1256,7 @@ func handleGetServiceInformation(ctx context.Context, request mcp.CallToolReques } cmd := fmt.Sprintf("service get %s", serviceID) - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get service information: %v", err)), nil } @@ -1056,7 +1277,7 @@ func handleDeleteService(ctx context.Context, request mcp.CallToolRequest) (*mcp return mcp.NewToolResultError("either service_id or all=true must be provided"), nil } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to delete service: %v", err)), nil } @@ -1115,7 +1336,7 @@ func handleUpdateService(ctx context.Context, request mcp.CallToolRequest) (*mcp cmd += " --local-redirect" } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to update service: %v", err)), nil } @@ -1155,239 +1376,9 @@ func handleGetDaemonStatus(ctx context.Context, request mcp.CallToolRequest) (*m cmd += " --brief" } - output, err := runCiliumDbgCommand(cmd, nodeName) + output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get daemon status: %v", err)), nil } return mcp.NewToolResultText(output), nil } - -func RegisterCiliumDbgTools(s *server.MCPServer) { - s.AddTool(mcp.NewTool("cilium_get_endpoint_details", - mcp.WithDescription("List the details of an endpoint in the cluster"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to get details for")), - mcp.WithString("labels", mcp.Description("The labels of the endpoint to get details for")), - mcp.WithString("output_format", mcp.Description("The output format of the endpoint details (json, yaml, jsonpath)")), - mcp.WithString("node_name", mcp.Description("The name of the node to get the endpoint details for")), - ), handleGetEndpointDetails) - - s.AddTool(mcp.NewTool("cilium_get_endpoint_logs", - mcp.WithDescription("Get the logs of an endpoint in the cluster"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to get logs for"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to get the endpoint logs for")), - ), handleGetEndpointLogs) - - s.AddTool(mcp.NewTool("cilium_get_endpoint_health", - mcp.WithDescription("Get the health of an endpoint in the cluster"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to get health for"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to get the endpoint health for")), - ), handleGetEndpointHealth) - - s.AddTool(mcp.NewTool("cilium_manage_endpoint_labels", - mcp.WithDescription("Manage the labels (add or delete) of an endpoint in the cluster"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage labels for"), mcp.Required()), - mcp.WithString("labels", mcp.Description("Space-separated labels to manage (e.g., 'key1=value1 key2=value2')"), mcp.Required()), - mcp.WithString("action", mcp.Description("The action to perform on the labels (add or delete)"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint labels on")), - ), handleManageEndpointLabels) - - s.AddTool(mcp.NewTool("cilium_manage_endpoint_config", - mcp.WithDescription("Manage the configuration of an endpoint in the cluster"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage configuration for"), mcp.Required()), - mcp.WithString("config", mcp.Description("The configuration to manage for the endpoint provided as a space-separated list of key-value pairs (e.g. 'DropNotification=false TraceNotification=false')"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint configuration on")), - ), handleManageEndpointConfiguration) - - s.AddTool(mcp.NewTool("cilium_disconnect_endpoint", - mcp.WithDescription("Disconnect an endpoint from the network"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to disconnect"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to disconnect the endpoint from")), - ), handleDisconnectEndpoint) - - s.AddTool(mcp.NewTool("cilium_list_identities", - mcp.WithDescription("List all identities in the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to list the identities for")), - ), handleListIdentities) - - s.AddTool(mcp.NewTool("cilium_get_identity_details", - mcp.WithDescription("Get the details of an identity in the cluster"), - mcp.WithString("identity_id", mcp.Description("The ID of the identity to get details for"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to get the identity details for")), - ), handleGetIdentityDetails) - - s.AddTool(mcp.NewTool("cilium_request_debugging_information", - mcp.WithDescription("Request debugging information for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the debugging information for")), - ), handleRequestDebuggingInformation) - - s.AddTool(mcp.NewTool("cilium_display_encryption_state", - mcp.WithDescription("Display the encryption state for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the encryption state for")), - ), handleDisplayEncryptionState) - - s.AddTool(mcp.NewTool("cilium_flush_ipsec_state", - mcp.WithDescription("Flush the IPsec state for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to flush the IPsec state for")), - ), handleFlushIPsecState) - - s.AddTool(mcp.NewTool("cilium_list_envoy_config", - mcp.WithDescription("List the Envoy configuration for a resource in the cluster"), - mcp.WithString("resource_name", mcp.Description("The name of the resource to get the Envoy configuration for"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to get the Envoy configuration for")), - ), handleListEnvoyConfig) - - s.AddTool(mcp.NewTool("cilium_fqdn_cache", - mcp.WithDescription("Manage the FQDN cache for the cluster"), - mcp.WithString("command", mcp.Description("The command to perform on the FQDN cache (list, clean, or a specific command)"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to manage the FQDN cache for")), - ), handleFQDNCache) - - s.AddTool(mcp.NewTool("cilium_show_dns_names", - mcp.WithDescription("Show the DNS names for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the DNS names for")), - ), handleShowDNSNames) - - s.AddTool(mcp.NewTool("cilium_list_ip_addresses", - mcp.WithDescription("List the IP addresses for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the IP addresses for")), - ), handleListIPAddresses) - - s.AddTool(mcp.NewTool("cilium_show_ip_cache_information", - mcp.WithDescription("Show the IP cache information for the cluster"), - mcp.WithString("cidr", mcp.Description("The CIDR of the IP to get cache information for")), - mcp.WithString("labels", mcp.Description("The labels of the IP to get cache information for")), - mcp.WithString("node_name", mcp.Description("The name of the node to get the IP cache information for")), - ), handleShowIPCacheInformation) - - s.AddTool(mcp.NewTool("cilium_delete_key_from_kv_store", - mcp.WithDescription("Delete a key from the kvstore for the cluster"), - mcp.WithString("key", mcp.Description("The key to delete from the kvstore"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the key from")), - ), handleDeleteKeyFromKVStore) - - s.AddTool(mcp.NewTool("cilium_get_kv_store_key", - mcp.WithDescription("Get a key from the kvstore for the cluster"), - mcp.WithString("key", mcp.Description("The key to get from the kvstore"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to get the key from")), - ), handleGetKVStoreKey) - - s.AddTool(mcp.NewTool("cilium_set_kv_store_key", - mcp.WithDescription("Set a key in the kvstore for the cluster"), - mcp.WithString("key", mcp.Description("The key to set in the kvstore"), mcp.Required()), - mcp.WithString("value", mcp.Description("The value to set in the kvstore"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to set the key in")), - ), handleSetKVStoreKey) - - s.AddTool(mcp.NewTool("cilium_show_load_information", - mcp.WithDescription("Show load information for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the load information for")), - ), handleShowLoadInformation) - - s.AddTool(mcp.NewTool("cilium_list_local_redirect_policies", - mcp.WithDescription("List local redirect policies for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the local redirect policies for")), - ), handleListLocalRedirectPolicies) - - s.AddTool(mcp.NewTool("cilium_list_bpf_map_events", - mcp.WithDescription("List BPF map events for the cluster"), - mcp.WithString("map_name", mcp.Description("The name of the BPF map to get events for"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to get the BPF map events for")), - ), handleListBPFMapEvents) - - s.AddTool(mcp.NewTool("cilium_get_bpf_map", - mcp.WithDescription("Get BPF map for the cluster"), - mcp.WithString("map_name", mcp.Description("The name of the BPF map to get"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to get the BPF map for")), - ), handleGetBPFMap) - - s.AddTool(mcp.NewTool("cilium_list_bpf_maps", - mcp.WithDescription("List BPF maps for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the BPF maps for")), - ), handleListBPFMaps) - - s.AddTool(mcp.NewTool("cilium_list_metrics", - mcp.WithDescription("List metrics for the cluster"), - mcp.WithString("match_pattern", mcp.Description("The match pattern to filter metrics by")), - mcp.WithString("node_name", mcp.Description("The name of the node to get the metrics for")), - ), handleListMetrics) - - s.AddTool(mcp.NewTool("cilium_list_cluster_nodes", - mcp.WithDescription("List cluster nodes for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the cluster nodes for")), - ), handleListClusterNodes) - - s.AddTool(mcp.NewTool("cilium_list_node_ids", - mcp.WithDescription("List node IDs for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the node IDs for")), - ), handleListNodeIds) - - s.AddTool(mcp.NewTool("cilium_display_policy_node_information", - mcp.WithDescription("Display policy node information for the cluster"), - mcp.WithString("labels", mcp.Description("The labels to get policy node information for")), - mcp.WithString("node_name", mcp.Description("The name of the node to get policy node information for")), - ), handleDisplayPolicyNodeInformation) - - s.AddTool(mcp.NewTool("cilium_delete_policy_rules", - mcp.WithDescription("Delete policy rules for the cluster"), - mcp.WithString("labels", mcp.Description("The labels to delete policy rules for")), - mcp.WithString("all", mcp.Description("Whether to delete all policy rules")), - mcp.WithString("node_name", mcp.Description("The name of the node to delete policy rules for")), - ), handleDeletePolicyRules) - - s.AddTool(mcp.NewTool("cilium_display_selectors", - mcp.WithDescription("Display selectors for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get selectors for")), - ), handleDisplaySelectors) - - s.AddTool(mcp.NewTool("cilium_list_xdp_cidr_filters", - mcp.WithDescription("List XDP CIDR filters for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the XDP CIDR filters for")), - ), handleListXDPCIDRFilters) - - s.AddTool(mcp.NewTool("cilium_update_xdp_cidr_filters", - mcp.WithDescription("Update XDP CIDR filters for the cluster"), - mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to update the XDP filters for"), mcp.Required()), - mcp.WithString("revision", mcp.Description("The revision of the XDP filters to update")), - mcp.WithString("node_name", mcp.Description("The name of the node to update the XDP filters for")), - ), handleUpdateXDPCIDRFilters) - - s.AddTool(mcp.NewTool("cilium_delete_xdp_cidr_filters", - mcp.WithDescription("Delete XDP CIDR filters for the cluster"), - mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to delete the XDP filters for"), mcp.Required()), - mcp.WithString("revision", mcp.Description("The revision of the XDP filters to delete")), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the XDP filters for")), - ), handleDeleteXDPCIDRFilters) - - s.AddTool(mcp.NewTool("cilium_validate_cilium_network_policies", - mcp.WithDescription("Validate Cilium network policies for the cluster"), - mcp.WithString("enable_k8s", mcp.Description("Whether to enable k8s API discovery")), - mcp.WithString("enable_k8s_api_discovery", mcp.Description("Whether to enable k8s API discovery")), - mcp.WithString("node_name", mcp.Description("The name of the node to validate the Cilium network policies for")), - ), handleValidateCiliumNetworkPolicies) - - s.AddTool(mcp.NewTool("cilium_list_pcap_recorders", - mcp.WithDescription("List PCAP recorders for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to get the PCAP recorders for")), - ), handleListPCAPRecorders) - - s.AddTool(mcp.NewTool("cilium_get_pcap_recorder", - mcp.WithDescription("Get a PCAP recorder for the cluster"), - mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to get"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to get the PCAP recorder for")), - ), handleGetPCAPRecorder) - - s.AddTool(mcp.NewTool("cilium_delete_pcap_recorder", - mcp.WithDescription("Delete a PCAP recorder for the cluster"), - mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to delete"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the PCAP recorder from")), - ), handleDeletePCAPRecorder) - - s.AddTool(mcp.NewTool("cilium_update_pcap_recorder", - mcp.WithDescription("Update a PCAP recorder for the cluster"), - mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to update"), mcp.Required()), - mcp.WithString("filters", mcp.Description("The filters to update the PCAP recorder with"), mcp.Required()), - mcp.WithString("caplen", mcp.Description("The caplen to update the PCAP recorder with")), - mcp.WithString("id", mcp.Description("The id to update the PCAP recorder with")), - mcp.WithString("node_name", mcp.Description("The name of the node to update the PCAP recorder on")), - ), handleUpdatePCAPRecorder) -} diff --git a/pkg/cilium/cilium_test.go b/pkg/cilium/cilium_test.go index 5b01846e..866de5d7 100644 --- a/pkg/cilium/cilium_test.go +++ b/pkg/cilium/cilium_test.go @@ -1,99 +1,269 @@ package cilium import ( + "context" + "errors" + "fmt" + "strings" "testing" + "github.com/kagent-dev/tools/internal/cmd" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Basic command construction tests for Cilium CLI commands -// Note: MCP handler tests are in cilium_mcp_test.go +func TestRegisterCiliumTools(t *testing.T) { + s := server.NewMCPServer("test-server", "v0.0.1") + RegisterTools(s) + // We can't directly check the tools, but we can ensure the call doesn't panic +} -func TestCiliumCommandConstruction(t *testing.T) { - t.Run("basic command construction patterns", func(t *testing.T) { - // Test that we can construct basic cilium commands - args := []string{"status"} - assert.Equal(t, "status", args[0]) +func TestHandleCiliumStatusAndVersion(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"status", "--timeout", "30s"}, "Cilium status: OK", nil) + mock.AddCommandString("cilium", []string{"version", "--timeout", "30s"}, "cilium version 1.14.0", nil) - // Test upgrade command with parameters - upgradeArgs := []string{"upgrade"} - if clusterName := "test-cluster"; clusterName != "" { - upgradeArgs = append(upgradeArgs, "--cluster-name", clusterName) - } - if datapathMode := "tunnel"; datapathMode != "" { - upgradeArgs = append(upgradeArgs, "--datapath-mode", datapathMode) - } + ctx = cmd.WithShellExecutor(ctx, mock) - expected := []string{"upgrade", "--cluster-name", "test-cluster", "--datapath-mode", "tunnel"} - assert.Equal(t, expected, upgradeArgs) - }) + result, err := handleCiliumStatusAndVersion(ctx, mcp.CallToolRequest{}) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) - t.Run("install command with parameters", func(t *testing.T) { - args := []string{"install"} - if clusterName := "test-cluster"; clusterName != "" { - args = append(args, "--set", "cluster.name="+clusterName) - } - if clusterID := "123"; clusterID != "" { - args = append(args, "--set", "cluster.id="+clusterID) - } - if datapathMode := "tunnel"; datapathMode != "" { - args = append(args, "--datapath-mode", datapathMode) + var textContent mcp.TextContent + var ok bool + for _, content := range result.Content { + if textContent, ok = content.(mcp.TextContent); ok { + break } + } + require.True(t, ok, "no text content in result") - expected := []string{"install", "--set", "cluster.name=test-cluster", "--set", "cluster.id=123", "--datapath-mode", "tunnel"} - assert.Equal(t, expected, args) - }) + assert.Contains(t, textContent.Text, "Cilium status: OK") + assert.Contains(t, textContent.Text, "cilium version 1.14.0") +} + +func TestHandleCiliumStatusAndVersionError(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"status", "--timeout", "30s"}, "", errors.New("command failed")) + mock.AddCommandString("cilium", []string{"version", "--timeout", "30s"}, "cilium version 1.14.0", nil) + + ctx = cmd.WithShellExecutor(ctx, mock) + + result, err := handleCiliumStatusAndVersion(ctx, mcp.CallToolRequest{}) + require.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "Error getting Cilium status") +} + +func TestHandleInstallCilium(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"install", "--timeout", "30s"}, "✓ Cilium was successfully installed!", nil) + + ctx = cmd.WithShellExecutor(ctx, mock) + + result, err := handleInstallCilium(ctx, mcp.CallToolRequest{}) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "✓ Cilium was successfully installed!") +} + +func TestHandleUninstallCilium(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"uninstall", "--timeout", "30s"}, "✓ Cilium was successfully uninstalled!", nil) + + ctx = cmd.WithShellExecutor(ctx, mock) + + result, err := handleUninstallCilium(ctx, mcp.CallToolRequest{}) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "✓ Cilium was successfully uninstalled!") +} + +func TestHandleUpgradeCilium(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"upgrade", "--timeout", "30s"}, "✓ Cilium was successfully upgraded!", nil) - t.Run("clustermesh connect command", func(t *testing.T) { - clusterName := "remote-cluster" - context := "remote-context" + ctx = cmd.WithShellExecutor(ctx, mock) - args := []string{"clustermesh", "connect", "--destination-cluster", clusterName} - if context != "" { - args = append(args, "--destination-context", context) + result, err := handleUpgradeCilium(ctx, mcp.CallToolRequest{}) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "✓ Cilium was successfully upgraded!") +} + +func TestHandleConnectToRemoteCluster(t *testing.T) { + ctx := context.Background() + + t.Run("success", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"clustermesh", "connect", "--destination-cluster", "my-cluster", "--timeout", "30s"}, "✓ Connected to cluster my-cluster!", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Arguments: map[string]any{ + "cluster_name": "my-cluster", + }, + }, } - expected := []string{"clustermesh", "connect", "--destination-cluster", "remote-cluster", "--destination-context", "remote-context"} - assert.Equal(t, expected, args) + result, err := handleConnectToRemoteCluster(ctx, req) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "✓ Connected to cluster my-cluster!") }) - t.Run("bgp commands", func(t *testing.T) { - peersArgs := []string{"bgp", "peers"} - routesArgs := []string{"bgp", "routes"} - - assert.Equal(t, []string{"bgp", "peers"}, peersArgs) - assert.Equal(t, []string{"bgp", "routes"}, routesArgs) + t.Run("missing cluster_name", func(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Arguments: map[string]any{}, + }, + } + result, err := handleConnectToRemoteCluster(ctx, req) + require.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "cluster_name parameter is required") }) } -func TestCiliumParameterValidation(t *testing.T) { - t.Run("cluster name validation", func(t *testing.T) { - clusterName := "" - if clusterName == "" { - assert.True(t, true, "cluster_name parameter should be required for connect operations") +func TestHandleDisconnectFromRemoteCluster(t *testing.T) { + ctx := context.Background() + + t.Run("success", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"clustermesh", "disconnect", "--destination-cluster", "my-cluster", "--timeout", "30s"}, "✓ Disconnected from cluster my-cluster!", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Arguments: map[string]any{ + "cluster_name": "my-cluster", + }, + }, } - clusterName = "valid-cluster" - if clusterName != "" { - assert.True(t, true, "valid cluster name should be accepted") + result, err := handleDisconnectRemoteCluster(ctx, req) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "✓ Disconnected from cluster my-cluster!") + }) + + t.Run("missing cluster_name", func(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Arguments: map[string]any{}, + }, } + result, err := handleDisconnectRemoteCluster(ctx, req) + require.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "cluster_name parameter is required") }) +} + +func TestHandleEnableHubble(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"hubble", "enable", "--timeout", "30s"}, "✓ Hubble was successfully enabled!", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Arguments: map[string]any{ + "enable": true, + }, + }, + } - t.Run("boolean parameter handling", func(t *testing.T) { - enableStr := "true" - enable := enableStr == "true" - assert.True(t, enable) + result, err := handleToggleHubble(ctx, req) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "✓ Hubble was successfully enabled!") +} - enableStr = "false" - enable = enableStr == "true" - assert.False(t, enable) +func TestHandleDisableHubble(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"hubble", "disable", "--timeout", "30s"}, "✓ Hubble was successfully disabled!", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Arguments: map[string]any{ + "enable": false, + }, + }, + } + result, err := handleToggleHubble(ctx, req) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "✓ Hubble was successfully disabled!") +} - // Default value handling - enableStr = "" - if enableStr == "" { - enableStr = "true" // default - } - enable = enableStr == "true" - assert.True(t, enable) +func TestHandleListBGPPeers(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"bgp", "peers", "--timeout", "30s"}, "listing BGP peers", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + result, err := handleListBGPPeers(ctx, mcp.CallToolRequest{}) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "listing BGP peers") +} + +func TestHandleListBGPRoutes(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"bgp", "routes", "--timeout", "30s"}, "listing BGP routes", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + result, err := handleListBGPRoutes(ctx, mcp.CallToolRequest{}) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "listing BGP routes") +} + +func TestRunCiliumCliWithContext(t *testing.T) { + ctx := context.Background() + t.Run("success", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"test", "--timeout", "30s"}, "success", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + result, err := runCiliumCliWithContext(ctx, "test") + require.NoError(t, err) + assert.Equal(t, "success", result) + }) + t.Run("error", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("cilium", []string{"test", "--timeout", "30s"}, "", fmt.Errorf("test error")) + ctx = cmd.WithShellExecutor(ctx, mock) + _, err := runCiliumCliWithContext(ctx, "test") + require.Error(t, err) + assert.Contains(t, err.Error(), "test error") }) } + +func getResultText(r *mcp.CallToolResult) string { + if r == nil || len(r.Content) == 0 { + return "" + } + if textContent, ok := r.Content[0].(mcp.TextContent); ok { + return strings.TrimSpace(textContent.Text) + } + return "" +} diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index b3a65e3e..06a9ac8a 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -5,13 +5,15 @@ import ( "fmt" "strings" + "github.com/kagent-dev/tools/internal/commands" + "github.com/kagent-dev/tools/internal/errors" + "github.com/kagent-dev/tools/internal/security" + "github.com/kagent-dev/tools/internal/telemetry" "github.com/kagent-dev/tools/pkg/utils" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) -var kubeConfig = "" // Global variable to hold kubeconfig path - // Helm list releases func handleHelmListReleases(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { namespace := mcp.ParseString(request, "namespace", "") @@ -69,6 +71,15 @@ func handleHelmListReleases(ctx context.Context, request mcp.CallToolRequest) (* result, err := runHelmCommand(ctx, args) if err != nil { + // Check if it's a structured error + if toolErr, ok := err.(*errors.ToolError); ok { + // Add namespace context if provided + if namespace != "" { + toolErr = toolErr.WithContext("namespace", namespace) + } + return toolErr.ToMCPResult(), nil + } + // Fallback for non-structured errors return mcp.NewToolResultError(fmt.Sprintf("Helm list command failed: %v", err)), nil } @@ -76,10 +87,24 @@ func handleHelmListReleases(ctx context.Context, request mcp.CallToolRequest) (* } func runHelmCommand(ctx context.Context, args []string) (string, error) { - if kubeConfig != "" { - args = append(args, "--kubeconfig", kubeConfig) + kubeconfigPath := utils.GetKubeconfig() + result, err := commands.NewCommandBuilder("helm"). + WithArgs(args...). + WithKubeconfig(kubeconfigPath). + Execute(ctx) + + if err != nil { + if toolErr, ok := err.(*errors.ToolError); ok { + if len(args) > 0 { + toolErr = toolErr.WithContext("helm_operation", args[0]) + } + toolErr = toolErr.WithContext("helm_args", args) + return "", toolErr + } + return "", err } - return utils.RunCommandWithContext(ctx, "helm", args) + + return result, nil } // Helm get release @@ -98,7 +123,7 @@ func handleHelmGetRelease(ctx context.Context, request mcp.CallToolRequest) (*mc args := []string{"get", resource, name, "-n", namespace} - result, err := utils.RunCommandWithContext(ctx, "helm", args) + result, err := runHelmCommand(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Helm get command failed: %v", err)), nil } @@ -122,6 +147,25 @@ func handleHelmUpgradeRelease(ctx context.Context, request mcp.CallToolRequest) return mcp.NewToolResultError("name and chart parameters are required"), nil } + // Validate release name + if err := security.ValidateHelmReleaseName(name); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid release name: %v", err)), nil + } + + // Validate namespace if provided + if namespace != "" { + if err := security.ValidateNamespace(namespace); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace: %v", err)), nil + } + } + + // Validate values file path if provided + if values != "" { + if err := security.ValidateFilePath(values); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid values file path: %v", err)), nil + } + } + args := []string{"upgrade", name, chart} if namespace != "" { @@ -156,7 +200,7 @@ func handleHelmUpgradeRelease(ctx context.Context, request mcp.CallToolRequest) args = append(args, "--wait") } - result, err := utils.RunCommandWithContext(ctx, "helm", args) + result, err := runHelmCommand(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Helm upgrade command failed: %v", err)), nil } @@ -185,7 +229,7 @@ func handleHelmUninstall(ctx context.Context, request mcp.CallToolRequest) (*mcp args = append(args, "--wait") } - result, err := utils.RunCommandWithContext(ctx, "helm", args) + result, err := runHelmCommand(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Helm uninstall command failed: %v", err)), nil } @@ -202,9 +246,19 @@ func handleHelmRepoAdd(ctx context.Context, request mcp.CallToolRequest) (*mcp.C return mcp.NewToolResultError("name and url parameters are required"), nil } + // Validate repository name + if err := security.ValidateHelmReleaseName(name); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid repository name: %v", err)), nil + } + + // Validate repository URL + if err := security.ValidateURL(url); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid repository URL: %v", err)), nil + } + args := []string{"repo", "add", name, url} - result, err := utils.RunCommandWithContext(ctx, "helm", args) + result, err := runHelmCommand(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Helm repo add command failed: %v", err)), nil } @@ -216,7 +270,7 @@ func handleHelmRepoAdd(ctx context.Context, request mcp.CallToolRequest) (*mcp.C func handleHelmRepoUpdate(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := []string{"repo", "update"} - result, err := utils.RunCommandWithContext(ctx, "helm", args) + result, err := runHelmCommand(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Helm repo update command failed: %v", err)), nil } @@ -225,8 +279,7 @@ func handleHelmRepoUpdate(ctx context.Context, request mcp.CallToolRequest) (*mc } // Register Helm tools -func RegisterHelmTools(s *server.MCPServer, kubeconfig string) { - kubeConfig = kubeconfig +func RegisterTools(s *server.MCPServer) { s.AddTool(mcp.NewTool("helm_list_releases", mcp.WithDescription("List Helm releases in a namespace"), @@ -240,14 +293,14 @@ func RegisterHelmTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("pending", mcp.Description("List pending releases")), mcp.WithString("filter", mcp.Description("A regular expression to filter releases by")), mcp.WithString("output", mcp.Description("The output format (e.g., 'json', 'yaml', 'table')")), - ), handleHelmListReleases) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_list_releases", handleHelmListReleases))) s.AddTool(mcp.NewTool("helm_get_release", mcp.WithDescription("Get extended information about a Helm release"), mcp.WithString("name", mcp.Description("The name of the release"), mcp.Required()), mcp.WithString("namespace", mcp.Description("The namespace of the release"), mcp.Required()), mcp.WithString("resource", mcp.Description("The resource to get (all, hooks, manifest, notes, values)")), - ), handleHelmGetRelease) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_get_release", handleHelmGetRelease))) s.AddTool(mcp.NewTool("helm_upgrade", mcp.WithDescription("Upgrade or install a Helm release"), @@ -260,7 +313,7 @@ func RegisterHelmTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("install", mcp.Description("Run an install if the release is not present")), mcp.WithString("dry_run", mcp.Description("Simulate an upgrade")), mcp.WithString("wait", mcp.Description("Wait for the upgrade to complete")), - ), handleHelmUpgradeRelease) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_upgrade", handleHelmUpgradeRelease))) s.AddTool(mcp.NewTool("helm_uninstall", mcp.WithDescription("Uninstall a Helm release"), @@ -268,15 +321,15 @@ func RegisterHelmTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("namespace", mcp.Description("The namespace of the release"), mcp.Required()), mcp.WithString("dry_run", mcp.Description("Simulate an uninstall")), mcp.WithString("wait", mcp.Description("Wait for the uninstall to complete")), - ), handleHelmUninstall) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_uninstall", handleHelmUninstall))) s.AddTool(mcp.NewTool("helm_repo_add", mcp.WithDescription("Add a Helm repository"), mcp.WithString("name", mcp.Description("The name of the repository"), mcp.Required()), mcp.WithString("url", mcp.Description("The URL of the repository"), mcp.Required()), - ), handleHelmRepoAdd) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_repo_add", handleHelmRepoAdd))) s.AddTool(mcp.NewTool("helm_repo_update", mcp.WithDescription("Update information of available charts locally from chart repositories"), - ), handleHelmRepoUpdate) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_repo_update", handleHelmRepoUpdate))) } diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go index 4a991657..3848de2a 100644 --- a/pkg/helm/helm_test.go +++ b/pkg/helm/helm_test.go @@ -4,22 +4,28 @@ import ( "context" "testing" - "github.com/kagent-dev/tools/pkg/utils" + "github.com/kagent-dev/tools/internal/cmd" "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestRegisterTools(t *testing.T) { + s := server.NewMCPServer("test-server", "v0.0.1") + RegisterTools(s) +} + // Test Helm List Releases func TestHandleHelmListReleases(t *testing.T) { t.Run("basic list releases", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION app1 default 1 2023-01-01 12:00:00.000000000 +0000 UTC deployed myapp-1.0.0 1.0.0 app2 kube-system 2 2023-01-02 12:00:00.000000000 +0000 UTC deployed system-2.0.0 2.0.0` - mock.AddCommandString("helm", []string{"list"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("helm", []string{"list", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} result, err := handleHelmListReleases(ctx, request) @@ -37,13 +43,13 @@ app2 kube-system 2 2023-01-02 12:00:00.000000000 +0000 UTC deplo callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"list"}, callLog[0].Args) + assert.Equal(t, []string{"list", "--timeout", "30s"}, callLog[0].Args) }) t.Run("list releases with namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("helm", []string{"list", "-n", "production"}, "production releases", nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("helm", []string{"list", "-n", "production", "--timeout", "30s"}, "production releases", nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -59,13 +65,13 @@ app2 kube-system 2 2023-01-02 12:00:00.000000000 +0000 UTC deplo callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"list", "-n", "production"}, callLog[0].Args) + assert.Equal(t, []string{"list", "-n", "production", "--timeout", "30s"}, callLog[0].Args) }) t.Run("list releases with all namespaces", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("helm", []string{"list", "-A"}, "all namespaces releases", nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("helm", []string{"list", "-A", "--timeout", "30s"}, "all namespaces releases", nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -81,13 +87,13 @@ app2 kube-system 2 2023-01-02 12:00:00.000000000 +0000 UTC deplo callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"list", "-A"}, callLog[0].Args) + assert.Equal(t, []string{"list", "-A", "--timeout", "30s"}, callLog[0].Args) }) t.Run("list releases with multiple flags", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("helm", []string{"list", "-A", "-a", "--failed", "-o", "json"}, `[{"name":"failed-app","status":"failed"}]`, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("helm", []string{"list", "-A", "-a", "--failed", "-o", "json", "--timeout", "30s"}, `[{"name":"failed-app","status":"failed"}]`, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -106,35 +112,35 @@ app2 kube-system 2 2023-01-02 12:00:00.000000000 +0000 UTC deplo callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"list", "-A", "-a", "--failed", "-o", "json"}, callLog[0].Args) + assert.Equal(t, []string{"list", "-A", "-a", "--failed", "-o", "json", "--timeout", "30s"}, callLog[0].Args) }) t.Run("helm command failure", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("helm", []string{"list"}, "", assert.AnError) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("helm", []string{"list", "--timeout", "30s"}, "", assert.AnError) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} result, err := handleHelmListReleases(ctx, request) assert.NoError(t, err) // MCP handlers should not return Go errors assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "Helm list command failed") + assert.Contains(t, getResultText(result), "**Helm Error**") }) } // Test Helm Get Release func TestHandleHelmGetRelease(t *testing.T) { t.Run("get release all resources", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `REVISION: 1 RELEASED: Mon Jan 01 12:00:00 UTC 2023 CHART: myapp-1.0.0 VALUES: replicaCount: 3` - mock.AddCommandString("helm", []string{"get", "all", "myapp", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("helm", []string{"get", "all", "myapp", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -152,13 +158,13 @@ replicaCount: 3` callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"get", "all", "myapp", "-n", "default"}, callLog[0].Args) + assert.Equal(t, []string{"get", "all", "myapp", "-n", "default", "--timeout", "30s"}, callLog[0].Args) }) t.Run("get release values only", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("helm", []string{"get", "values", "myapp", "-n", "default"}, "replicaCount: 3", nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("helm", []string{"get", "values", "myapp", "-n", "default", "--timeout", "30s"}, "replicaCount: 3", nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -176,12 +182,12 @@ replicaCount: 3` callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"get", "values", "myapp", "-n", "default"}, callLog[0].Args) + assert.Equal(t, []string{"get", "values", "myapp", "-n", "default", "--timeout", "30s"}, callLog[0].Args) }) t.Run("missing required parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) // Test missing name request := mcp.CallToolRequest{} @@ -213,7 +219,7 @@ replicaCount: 3` // Test Helm Upgrade Release func TestHandleHelmUpgradeRelease(t *testing.T) { t.Run("basic upgrade", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `Release "myapp" has been upgraded. Happy Helming! NAME: myapp LAST DEPLOYED: Mon Jan 01 12:00:00 UTC 2023 @@ -221,8 +227,8 @@ NAMESPACE: default STATUS: deployed REVISION: 2` - mock.AddCommandString("helm", []string{"upgrade", "myapp", "stable/myapp"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("helm", []string{"upgrade", "myapp", "stable/myapp", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -240,11 +246,11 @@ REVISION: 2` callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"upgrade", "myapp", "stable/myapp"}, callLog[0].Args) + assert.Equal(t, []string{"upgrade", "myapp", "stable/myapp", "--timeout", "30s"}, callLog[0].Args) }) t.Run("upgrade with all options", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedArgs := []string{ "upgrade", "myapp", "stable/myapp", "-n", "production", @@ -255,9 +261,10 @@ REVISION: 2` "--install", "--dry-run", "--wait", + "--timeout", "30s", } - mock.AddCommandString("helm", expectedArgs, "dry run output", nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("helm", expectedArgs, "Upgraded with options", nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -284,14 +291,14 @@ REVISION: 2` assert.Equal(t, expectedArgs, callLog[0].Args) }) - t.Run("missing required parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + t.Run("missing required parameters for upgrade", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) + // Test missing chart request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ "name": "myapp", - // Missing chart } result, err := handleHelmUpgradeRelease(ctx, request) @@ -308,11 +315,11 @@ REVISION: 2` // Test Helm Uninstall func TestHandleHelmUninstall(t *testing.T) { t.Run("basic uninstall", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `release "myapp" uninstalled` - mock.AddCommandString("helm", []string{"uninstall", "myapp", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("helm", []string{"uninstall", "myapp", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -330,14 +337,14 @@ func TestHandleHelmUninstall(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"uninstall", "myapp", "-n", "default"}, callLog[0].Args) + assert.Equal(t, []string{"uninstall", "myapp", "-n", "default", "--timeout", "30s"}, callLog[0].Args) }) t.Run("uninstall with options", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedArgs := []string{"uninstall", "myapp", "-n", "production", "--dry-run", "--wait"} + mock := cmd.NewMockShellExecutor() + expectedArgs := []string{"uninstall", "myapp", "-n", "production", "--dry-run", "--wait", "--timeout", "30s"} mock.AddCommandString("helm", expectedArgs, "dry run uninstall", nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -358,21 +365,51 @@ func TestHandleHelmUninstall(t *testing.T) { assert.Equal(t, "helm", callLog[0].Command) assert.Equal(t, expectedArgs, callLog[0].Args) }) + + t.Run("missing required parameters for uninstall", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) + + // Test missing name + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "namespace": "default", + } + + result, err := handleHelmUninstall(ctx, request) + assert.NoError(t, err) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "name and namespace parameters are required") + + // Test missing namespace + request.Params.Arguments = map[string]interface{}{ + "name": "myapp", + } + + result, err = handleHelmUninstall(ctx, request) + assert.NoError(t, err) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "name and namespace parameters are required") + + // Verify no commands were executed + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) + }) } // Test Helm Repo Add func TestHandleHelmRepoAdd(t *testing.T) { - t.Run("add repository", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `"stable" has been added to your repositories` + t.Run("basic repo add", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `"my-repo" has been added to your repositories` - mock.AddCommandString("helm", []string{"repo", "add", "stable", "https://charts.helm.sh/stable"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("helm", []string{"repo", "add", "my-repo", "https://charts.example.com/", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ - "name": "stable", - "url": "https://charts.helm.sh/stable", + "name": "my-repo", + "url": "https://charts.example.com/", } result, err := handleHelmRepoAdd(ctx, request) @@ -385,17 +422,17 @@ func TestHandleHelmRepoAdd(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"repo", "add", "stable", "https://charts.helm.sh/stable"}, callLog[0].Args) + assert.Equal(t, []string{"repo", "add", "my-repo", "https://charts.example.com/", "--timeout", "30s"}, callLog[0].Args) }) - t.Run("missing required parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + t.Run("missing required parameters for repo add", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) + // Test missing name request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ - "name": "stable", - // Missing url + "url": "https://charts.example.com/", } result, err := handleHelmRepoAdd(ctx, request) @@ -411,27 +448,26 @@ func TestHandleHelmRepoAdd(t *testing.T) { // Test Helm Repo Update func TestHandleHelmRepoUpdate(t *testing.T) { - t.Run("update repositories", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + t.Run("basic repo update", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() expectedOutput := `Hang tight while we grab the latest from your chart repositories... -...Successfully got an update from the "stable" chart repository -Update Complete. ⎈Happy Helming!⎈` +...Successfully got an update from the "my-repo" chart repository` - mock.AddCommandString("helm", []string{"repo", "update"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("helm", []string{"repo", "update", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} result, err := handleHelmRepoUpdate(ctx, request) assert.NoError(t, err) assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "Update Complete") + assert.Contains(t, getResultText(result), "Successfully got an update") // Verify the correct command was called callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"repo", "update"}, callLog[0].Args) + assert.Equal(t, []string{"repo", "update", "--timeout", "30s"}, callLog[0].Args) }) } diff --git a/pkg/istio/istio.go b/pkg/istio/istio.go index 2f198aac..680d83ca 100644 --- a/pkg/istio/istio.go +++ b/pkg/istio/istio.go @@ -5,13 +5,13 @@ import ( "fmt" "strings" + "github.com/kagent-dev/tools/internal/commands" + "github.com/kagent-dev/tools/internal/telemetry" "github.com/kagent-dev/tools/pkg/utils" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) -var kubeConfig = "" // Global variable to hold kubeconfig path - // Istio proxy status func handleIstioProxyStatus(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { podName := mcp.ParseString(request, "pod_name", "") @@ -36,11 +36,11 @@ func handleIstioProxyStatus(ctx context.Context, request mcp.CallToolRequest) (* } func runIstioCtl(ctx context.Context, args []string) (string, error) { - if kubeConfig != "" { - args = append(args, "--kubeconfig", kubeConfig) - } - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) - return result, err + kubeconfigPath := utils.GetKubeconfig() + return commands.NewCommandBuilder("istioctl"). + WithArgs(args...). + WithKubeconfig(kubeconfigPath). + Execute(ctx) } // Istio proxy config @@ -61,7 +61,7 @@ func handleIstioProxyConfig(ctx context.Context, request mcp.CallToolRequest) (* args = append(args, podName) } - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl proxy-config failed: %v", err)), nil } @@ -75,7 +75,7 @@ func handleIstioInstall(ctx context.Context, request mcp.CallToolRequest) (*mcp. args := []string{"install", "--set", fmt.Sprintf("profile=%s", profile), "-y"} - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl install failed: %v", err)), nil } @@ -89,7 +89,7 @@ func handleIstioGenerateManifest(ctx context.Context, request mcp.CallToolReques args := []string{"manifest", "generate", "--set", fmt.Sprintf("profile=%s", profile)} - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl manifest generate failed: %v", err)), nil } @@ -110,7 +110,7 @@ func handleIstioAnalyzeClusterConfiguration(ctx context.Context, request mcp.Cal args = append(args, "-n", namespace) } - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl analyze failed: %v", err)), nil } @@ -128,7 +128,7 @@ func handleIstioVersion(ctx context.Context, request mcp.CallToolRequest) (*mcp. args = append(args, "--short") } - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl version failed: %v", err)), nil } @@ -140,7 +140,7 @@ func handleIstioVersion(ctx context.Context, request mcp.CallToolRequest) (*mcp. func handleIstioRemoteClusters(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := []string{"remote-clusters"} - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl remote-clusters failed: %v", err)), nil } @@ -161,7 +161,7 @@ func handleWaypointList(ctx context.Context, request mcp.CallToolRequest) (*mcp. args = append(args, "-n", namespace) } - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl waypoint list failed: %v", err)), nil } @@ -191,7 +191,7 @@ func handleWaypointGenerate(ctx context.Context, request mcp.CallToolRequest) (* args = append(args, "--for", trafficType) } - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl waypoint generate failed: %v", err)), nil } @@ -214,7 +214,7 @@ func handleWaypointApply(ctx context.Context, request mcp.CallToolRequest) (*mcp args = append(args, "--enroll-namespace") } - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl waypoint apply failed: %v", err)), nil } @@ -245,7 +245,7 @@ func handleWaypointDelete(ctx context.Context, request mcp.CallToolRequest) (*mc args = append(args, "-n", namespace) - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl waypoint delete failed: %v", err)), nil } @@ -270,7 +270,7 @@ func handleWaypointStatus(ctx context.Context, request mcp.CallToolRequest) (*mc args = append(args, "-n", namespace) - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("istioctl waypoint status failed: %v", err)), nil } @@ -283,30 +283,29 @@ func handleZtunnelConfig(ctx context.Context, request mcp.CallToolRequest) (*mcp namespace := mcp.ParseString(request, "namespace", "") configType := mcp.ParseString(request, "config_type", "all") - args := []string{"ztunnel-config", configType} + args := []string{"ztunnel", "config", configType} if namespace != "" { args = append(args, "-n", namespace) } - result, err := utils.RunCommandWithContext(ctx, "istioctl", args) + result, err := runIstioCtl(ctx, args) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("istioctl ztunnel-config failed: %v", err)), nil + return mcp.NewToolResultError(fmt.Sprintf("istioctl ztunnel config failed: %v", err)), nil } return mcp.NewToolResultText(result), nil } // Register Istio tools -func RegisterIstioTools(s *server.MCPServer, kubeconfig string) { - kubeConfig = kubeconfig +func RegisterTools(s *server.MCPServer) { // Istio proxy status s.AddTool(mcp.NewTool("istio_proxy_status", mcp.WithDescription("Get Envoy proxy status for pods, retrieves last sent and acknowledged xDS sync from Istiod to each Envoy in the mesh"), mcp.WithString("pod_name", mcp.Description("Name of the pod to get proxy status for")), mcp.WithString("namespace", mcp.Description("Namespace of the pod")), - ), handleIstioProxyStatus) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_proxy_status", handleIstioProxyStatus))) // Istio proxy config s.AddTool(mcp.NewTool("istio_proxy_config", @@ -314,79 +313,62 @@ func RegisterIstioTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("pod_name", mcp.Description("Name of the pod to get proxy configuration for"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace of the pod")), mcp.WithString("config_type", mcp.Description("Type of configuration (all, bootstrap, cluster, ecds, listener, log, route, secret)")), - ), handleIstioProxyConfig) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_proxy_config", handleIstioProxyConfig))) // Istio install s.AddTool(mcp.NewTool("istio_install_istio", mcp.WithDescription("Install Istio with a specified configuration profile"), mcp.WithString("profile", mcp.Description("Istio configuration profile (ambient, default, demo, minimal, empty)")), - ), handleIstioInstall) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_install_istio", handleIstioInstall))) // Istio generate manifest s.AddTool(mcp.NewTool("istio_generate_manifest", - mcp.WithDescription("Generate an Istio install manifest"), + mcp.WithDescription("Generate Istio manifest for a given profile"), mcp.WithString("profile", mcp.Description("Istio configuration profile (ambient, default, demo, minimal, empty)")), - ), handleIstioGenerateManifest) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_generate_manifest", handleIstioGenerateManifest))) // Istio analyze s.AddTool(mcp.NewTool("istio_analyze_cluster_configuration", - mcp.WithDescription("Analyze live cluster configuration for potential issues"), - mcp.WithString("namespace", mcp.Description("Namespace to analyze")), - mcp.WithString("all_namespaces", mcp.Description("Analyze all namespaces (true/false)")), - ), handleIstioAnalyzeClusterConfiguration) + mcp.WithDescription("Analyze Istio cluster configuration for issues"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_analyze_cluster_configuration", handleIstioAnalyzeClusterConfiguration))) // Istio version s.AddTool(mcp.NewTool("istio_version", - mcp.WithDescription("Get Istio CLI client version, control plane and data plane versions"), - mcp.WithString("short", mcp.Description("Show short version format (true/false)")), - ), handleIstioVersion) + mcp.WithDescription("Get Istio version information"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_version", handleIstioVersion))) // Istio remote clusters s.AddTool(mcp.NewTool("istio_remote_clusters", - mcp.WithDescription("List remote clusters each istiod instance is connected to"), - ), handleIstioRemoteClusters) + mcp.WithDescription("List remote clusters registered with Istio"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_remote_clusters", handleIstioRemoteClusters))) // Waypoint list s.AddTool(mcp.NewTool("istio_list_waypoints", - mcp.WithDescription("List managed waypoint configurations in the cluster"), - mcp.WithString("namespace", mcp.Description("Namespace to list waypoints for")), - mcp.WithString("all_namespaces", mcp.Description("List waypoints for all namespaces (true/false)")), - ), handleWaypointList) + mcp.WithDescription("List all waypoints in the mesh"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_list_waypoints", handleWaypointList))) // Waypoint generate s.AddTool(mcp.NewTool("istio_generate_waypoint", - mcp.WithDescription("Generate a waypoint configuration as YAML"), - mcp.WithString("namespace", mcp.Description("Namespace to generate the waypoint for"), mcp.Required()), - mcp.WithString("name", mcp.Description("Name of the waypoint to generate")), - mcp.WithString("traffic_type", mcp.Description("Traffic type for the waypoint (all, inbound, outbound)")), - ), handleWaypointGenerate) + mcp.WithDescription("Generate a waypoint resource YAML"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_generate_waypoint", handleWaypointGenerate))) // Waypoint apply s.AddTool(mcp.NewTool("istio_apply_waypoint", - mcp.WithDescription("Apply a waypoint configuration to a cluster"), - mcp.WithString("namespace", mcp.Description("Namespace to apply the waypoint to"), mcp.Required()), - mcp.WithString("enroll_namespace", mcp.Description("Label the namespace with the waypoint name (true/false)")), - ), handleWaypointApply) + mcp.WithDescription("Apply a waypoint resource to the cluster"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_apply_waypoint", handleWaypointApply))) // Waypoint delete s.AddTool(mcp.NewTool("istio_delete_waypoint", - mcp.WithDescription("Delete waypoint configurations from a cluster"), - mcp.WithString("namespace", mcp.Description("Namespace to delete waypoints from"), mcp.Required()), - mcp.WithString("names", mcp.Description("Comma-separated list of waypoint names to delete")), - mcp.WithString("all", mcp.Description("Delete all waypoints in the namespace (true/false)")), - ), handleWaypointDelete) + mcp.WithDescription("Delete a waypoint resource from the cluster"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_delete_waypoint", handleWaypointDelete))) // Waypoint status s.AddTool(mcp.NewTool("istio_waypoint_status", - mcp.WithDescription("Get status of a waypoint"), - mcp.WithString("namespace", mcp.Description("Namespace of the waypoint"), mcp.Required()), - mcp.WithString("name", mcp.Description("Name of the waypoint to get status for")), - ), handleWaypointStatus) + mcp.WithDescription("Get the status of a waypoint resource"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_waypoint_status", handleWaypointStatus))) // Ztunnel config s.AddTool(mcp.NewTool("istio_ztunnel_config", - mcp.WithDescription("Get ztunnel configuration"), - mcp.WithString("namespace", mcp.Description("Namespace of the pod")), - mcp.WithString("config_type", mcp.Description("Type of configuration (all, bootstrap, cluster, ecds, listener, log, route, secret)")), - ), handleZtunnelConfig) + mcp.WithDescription("Get the ztunnel configuration for a namespace"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_ztunnel_config", handleZtunnelConfig))) } diff --git a/pkg/istio/istio_test.go b/pkg/istio/istio_test.go index 2adaf999..02efbee4 100644 --- a/pkg/istio/istio_test.go +++ b/pkg/istio/istio_test.go @@ -4,299 +4,185 @@ import ( "context" "testing" - "github.com/kagent-dev/tools/pkg/utils" + "github.com/kagent-dev/tools/internal/cmd" "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// Helper function to extract text content from MCP result -func getResultText(result *mcp.CallToolResult) string { - if result == nil || len(result.Content) == 0 { - return "" - } - if textContent, ok := result.Content[0].(mcp.TextContent); ok { - return textContent.Text - } - return "" +func TestRegisterTools(t *testing.T) { + s := server.NewMCPServer("test-server", "v0.0.1") + RegisterTools(s) } -// Test Istio Proxy Status func TestHandleIstioProxyStatus(t *testing.T) { + ctx := context.Background() + t.Run("basic proxy status", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `NAME CDS LDS EDS RDS ISTIOD VERSION -app-1 SYNCED SYNCED SYNCED SYNCED istiod-68d5d5b5fc-7vf6n 1.18.0 -app-2 SYNCED SYNCED SYNCED SYNCED istiod-68d5d5b5fc-7vf6n 1.18.0` + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"proxy-status", "--timeout", "30s"}, "Proxy status output", nil) - mock.AddCommandString("istioctl", []string{"proxy-status"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) - request := mcp.CallToolRequest{} - result, err := handleIstioProxyStatus(ctx, request) + result, err := handleIstioProxyStatus(ctx, mcp.CallToolRequest{}) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, result) assert.False(t, result.IsError) - - // Verify the expected output - content := getResultText(result) - assert.Contains(t, content, "app-1") - assert.Contains(t, content, "SYNCED") - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"proxy-status"}, callLog[0].Args) }) t.Run("proxy status with namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `NAME CDS LDS EDS RDS ISTIOD VERSION -app-1 SYNCED SYNCED SYNCED SYNCED istiod-68d5d5b5fc-7vf6n 1.18.0` + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"proxy-status", "-n", "istio-system", "--timeout", "30s"}, "Proxy status output", nil) - mock.AddCommandString("istioctl", []string{"proxy-status", "-n", "production"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ - "namespace": "production", + "namespace": "istio-system", } result, err := handleIstioProxyStatus(ctx, request) - assert.NoError(t, err) + require.NoError(t, err) + assert.NotNil(t, result) assert.False(t, result.IsError) - - // Verify the correct command was called with namespace - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"proxy-status", "-n", "production"}, callLog[0].Args) }) - t.Run("proxy status with pod name and namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `NAME CDS LDS EDS RDS ISTIOD VERSION -app-1 SYNCED SYNCED SYNCED SYNCED istiod-68d5d5b5fc-7vf6n 1.18.0` + t.Run("proxy status with pod name", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"proxy-status", "-n", "default", "test-pod", "--timeout", "30s"}, "Proxy status output", nil) - mock.AddCommandString("istioctl", []string{"proxy-status", "-n", "production", "app-1"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ - "namespace": "production", - "pod_name": "app-1", + "pod_name": "test-pod", + "namespace": "default", } result, err := handleIstioProxyStatus(ctx, request) - assert.NoError(t, err) + require.NoError(t, err) + assert.NotNil(t, result) assert.False(t, result.IsError) - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"proxy-status", "-n", "production", "app-1"}, callLog[0].Args) }) +} - t.Run("istioctl command failure", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"proxy-status"}, "", assert.AnError) - ctx := utils.WithShellExecutor(context.Background(), mock) +func TestHandleIstioProxyConfig(t *testing.T) { + ctx := context.Background() - request := mcp.CallToolRequest{} - result, err := handleIstioProxyStatus(ctx, request) + t.Run("missing pod_name parameter", func(t *testing.T) { + result, err := handleIstioProxyConfig(ctx, mcp.CallToolRequest{}) - assert.NoError(t, err) // MCP handlers should not return Go errors + require.NoError(t, err) + assert.NotNil(t, result) assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "istioctl proxy-status failed") }) -} -// Test Istio Proxy Config -func TestHandleIstioProxyConfig(t *testing.T) { - t.Run("proxy config all", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `CLUSTER NAME DIRECTION TYPE DESTINATION RULE -outbound|80||kubernetes.default.svc.cluster.local outbound EDS -inbound|80|| inbound EDS` + t.Run("proxy config with pod name", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"proxy-config", "all", "test-pod", "--timeout", "30s"}, "Proxy config output", nil) - mock.AddCommandString("istioctl", []string{"proxy-config", "all", "app-1"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ - "pod_name": "app-1", + "pod_name": "test-pod", } result, err := handleIstioProxyConfig(ctx, request) - assert.NoError(t, err) + require.NoError(t, err) + assert.NotNil(t, result) assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "CLUSTER NAME") - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"proxy-config", "all", "app-1"}, callLog[0].Args) }) - t.Run("proxy config with namespace and config type", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `CLUSTER NAME DIRECTION TYPE DESTINATION RULE -outbound|80||kubernetes.default.svc.cluster.local outbound EDS` + t.Run("proxy config with namespace", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"proxy-config", "cluster", "test-pod.default", "--timeout", "30s"}, "Proxy config output", nil) - mock.AddCommandString("istioctl", []string{"proxy-config", "cluster", "app-1.production"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ - "pod_name": "app-1", - "namespace": "production", + "pod_name": "test-pod", + "namespace": "default", "config_type": "cluster", } result, err := handleIstioProxyConfig(ctx, request) - assert.NoError(t, err) + require.NoError(t, err) + assert.NotNil(t, result) assert.False(t, result.IsError) - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"proxy-config", "cluster", "app-1.production"}, callLog[0].Args) - }) - - t.Run("missing required parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - // Missing pod_name - } - - result, err := handleIstioProxyConfig(ctx, request) - assert.NoError(t, err) - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "pod_name parameter is required") - - // Verify no commands were executed - callLog := mock.GetCallLog() - assert.Len(t, callLog, 0) }) } -// Test Istio Install func TestHandleIstioInstall(t *testing.T) { - t.Run("basic install", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `✔ Istio core installed -✔ Istiod installed -✔ Ingress gateways installed -✔ Installation complete` + ctx := context.Background() - mock.AddCommandString("istioctl", []string{"install", "--set", "profile=default", "-y"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + t.Run("install with default profile", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"install", "--set", "profile=default", "-y", "--timeout", "30s"}, "Install completed", nil) - request := mcp.CallToolRequest{} - result, err := handleIstioInstall(ctx, request) + ctx = cmd.WithShellExecutor(ctx, mock) - assert.NoError(t, err) - assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "Installation complete") + result, err := handleIstioInstall(ctx, mcp.CallToolRequest{}) - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"install", "--set", "profile=default", "-y"}, callLog[0].Args) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) }) - t.Run("install with profile", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `✔ Istio core installed -✔ Installation complete` + t.Run("install with custom profile", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"install", "--set", "profile=demo", "-y", "--timeout", "30s"}, "Install completed", nil) - mock.AddCommandString("istioctl", []string{"install", "--set", "profile=minimal", "-y"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ - "profile": "minimal", + "profile": "demo", } result, err := handleIstioInstall(ctx, request) - assert.NoError(t, err) + require.NoError(t, err) + assert.NotNil(t, result) assert.False(t, result.IsError) - - // Verify the correct command was called with profile - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"install", "--set", "profile=minimal", "-y"}, callLog[0].Args) }) } -// Test Istio Analyze -func TestHandleIstioAnalyzeClusterConfiguration(t *testing.T) { - t.Run("basic analyze", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `✔ No validation issues found when analyzing namespace: default.` - - mock.AddCommandString("istioctl", []string{"analyze"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - result, err := handleIstioAnalyzeClusterConfiguration(ctx, request) +func TestHandleIstioGenerateManifest(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() - assert.NoError(t, err) - assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "No validation issues found") + mock.AddCommandString("istioctl", []string{"manifest", "generate", "--set", "profile=minimal", "--timeout", "30s"}, "Generated manifest", nil) - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"analyze"}, callLog[0].Args) - }) + ctx = cmd.WithShellExecutor(ctx, mock) - t.Run("analyze with namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `✔ No validation issues found when analyzing namespace: production.` - - mock.AddCommandString("istioctl", []string{"analyze", "-n", "production"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "production", - } + request := mcp.CallToolRequest{} + request.Params.Arguments = map[string]interface{}{ + "profile": "minimal", + } - result, err := handleIstioAnalyzeClusterConfiguration(ctx, request) + result, err := handleIstioGenerateManifest(ctx, request) - assert.NoError(t, err) - assert.False(t, result.IsError) + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) +} - // Verify the correct command was called with namespace - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"analyze", "-n", "production"}, callLog[0].Args) - }) +func TestHandleIstioAnalyzeClusterConfiguration(t *testing.T) { + ctx := context.Background() t.Run("analyze all namespaces", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `✔ No validation issues found when analyzing all namespaces.` + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"analyze", "-A", "--timeout", "30s"}, "Analysis output", nil) - mock.AddCommandString("istioctl", []string{"analyze", "-A"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ @@ -305,565 +191,167 @@ func TestHandleIstioAnalyzeClusterConfiguration(t *testing.T) { result, err := handleIstioAnalyzeClusterConfiguration(ctx, request) - assert.NoError(t, err) - assert.False(t, result.IsError) - - // Verify the correct command was called with -A flag - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"analyze", "-A"}, callLog[0].Args) - }) -} - -// Test Istio Version -func TestHandleIstioVersion(t *testing.T) { - t.Run("version detailed output", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `client version: 1.18.0 -control plane version: 1.18.0 -data plane version: 1.18.0 (2 proxies)` - - mock.AddCommandString("istioctl", []string{"version"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - result, err := handleIstioVersion(ctx, request) - - assert.NoError(t, err) - assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "client version: 1.18.0") - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"version"}, callLog[0].Args) - }) - - t.Run("version short output", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `1.18.0` - - mock.AddCommandString("istioctl", []string{"version", "--short"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "short": "true", - } - - result, err := handleIstioVersion(ctx, request) - - assert.NoError(t, err) - assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "1.18.0") - - // Verify the correct command was called with --short flag - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"version", "--short"}, callLog[0].Args) - }) -} - -// Test Waypoint List -func TestHandleWaypointList(t *testing.T) { - t.Run("list waypoints", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `NAMESPACE NAME TRAFFIC TYPE -default waypoint ALL -production waypoint INBOUND` - - mock.AddCommandString("istioctl", []string{"waypoint", "list"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - result, err := handleWaypointList(ctx, request) - - assert.NoError(t, err) - assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "NAMESPACE") - assert.Contains(t, getResultText(result), "waypoint") - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"waypoint", "list"}, callLog[0].Args) - }) - - t.Run("list waypoints in namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `NAMESPACE NAME TRAFFIC TYPE -production waypoint INBOUND` - - mock.AddCommandString("istioctl", []string{"waypoint", "list", "-n", "production"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "production", - } - - result, err := handleWaypointList(ctx, request) - - assert.NoError(t, err) - assert.False(t, result.IsError) - - // Verify the correct command was called with namespace - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"waypoint", "list", "-n", "production"}, callLog[0].Args) - }) -} - -// Test Waypoint Generate -func TestHandleWaypointGenerate(t *testing.T) { - t.Run("generate waypoint", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `apiVersion: gateway.networking.k8s.io/v1beta1 -kind: Gateway -metadata: - name: waypoint - namespace: production -spec: - gatewayClassName: istio-waypoint` - - mock.AddCommandString("istioctl", []string{"waypoint", "generate", "waypoint", "-n", "production", "--for", "all"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "production", - } - - result, err := handleWaypointGenerate(ctx, request) - - assert.NoError(t, err) + require.NoError(t, err) + assert.NotNil(t, result) assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "apiVersion: gateway.networking.k8s.io/v1beta1") - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"waypoint", "generate", "waypoint", "-n", "production", "--for", "all"}, callLog[0].Args) - }) - - t.Run("missing required parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - // Missing namespace - } - - result, err := handleWaypointGenerate(ctx, request) - assert.NoError(t, err) - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "namespace parameter is required") - - // Verify no commands were executed - callLog := mock.GetCallLog() - assert.Len(t, callLog, 0) }) -} -// Test Waypoint Apply -func TestHandleWaypointApply(t *testing.T) { - t.Run("basic waypoint apply", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `waypoint/waypoint applied` + t.Run("analyze specific namespace", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"analyze", "-n", "default", "--timeout", "30s"}, "Analysis output", nil) - mock.AddCommandString("istioctl", []string{"waypoint", "apply", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ "namespace": "default", } - result, err := handleWaypointApply(ctx, request) + result, err := handleIstioAnalyzeClusterConfiguration(ctx, request) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, result) assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "applied") - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"waypoint", "apply", "-n", "default"}, callLog[0].Args) - }) - - t.Run("waypoint apply with enroll namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `waypoint/waypoint applied -namespace/default labeled with istio.io/use-waypoint=waypoint` - - mock.AddCommandString("istioctl", []string{"waypoint", "apply", "-n", "default", "--enroll-namespace"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "default", - "enroll_namespace": "true", - } - - result, err := handleWaypointApply(ctx, request) - - assert.NoError(t, err) - assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "applied") - - // Verify the correct command was called with --enroll-namespace flag - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"waypoint", "apply", "-n", "default", "--enroll-namespace"}, callLog[0].Args) - }) - - t.Run("missing namespace parameter", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - // Missing namespace - } - - result, err := handleWaypointApply(ctx, request) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "namespace parameter is required") - - // Verify no commands were executed - callLog := mock.GetCallLog() - assert.Len(t, callLog, 0) - }) - - t.Run("istioctl command failure", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"waypoint", "apply", "-n", "default"}, "", assert.AnError) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "default", - } - - result, err := handleWaypointApply(ctx, request) - - assert.NoError(t, err) // MCP handlers should not return Go errors - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "istioctl waypoint apply failed") }) } -// Test Waypoint Delete -func TestHandleWaypointDelete(t *testing.T) { - t.Run("delete all waypoints", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `waypoint/waypoint deleted` +func TestHandleIstioVersion(t *testing.T) { + ctx := context.Background() - mock.AddCommandString("istioctl", []string{"waypoint", "delete", "--all", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + t.Run("version full", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"version", "--timeout", "30s"}, "Version output", nil) - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "default", - "all": "true", - } + ctx = cmd.WithShellExecutor(ctx, mock) - result, err := handleWaypointDelete(ctx, request) + result, err := handleIstioVersion(ctx, mcp.CallToolRequest{}) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, result) assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "deleted") - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"waypoint", "delete", "--all", "-n", "default"}, callLog[0].Args) }) - t.Run("delete specific waypoints", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `waypoint/waypoint1 deleted -waypoint/waypoint2 deleted` + t.Run("version short", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"version", "--short", "--timeout", "30s"}, "1.18.0", nil) - mock.AddCommandString("istioctl", []string{"waypoint", "delete", "waypoint1", "waypoint2", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ - "namespace": "default", - "names": "waypoint1,waypoint2", + "short": "true", } - result, err := handleWaypointDelete(ctx, request) - - assert.NoError(t, err) - assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "deleted") - - // Verify the correct command was called with specific names - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"waypoint", "delete", "waypoint1", "waypoint2", "-n", "default"}, callLog[0].Args) - }) - - t.Run("missing namespace parameter", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - // Missing namespace - } + result, err := handleIstioVersion(ctx, request) - result, err := handleWaypointDelete(ctx, request) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, result) - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "namespace parameter is required") - - // Verify no commands were executed - callLog := mock.GetCallLog() - assert.Len(t, callLog, 0) - }) - - t.Run("istioctl command failure", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"waypoint", "delete", "--all", "-n", "default"}, "", assert.AnError) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "default", - "all": "true", - } - - result, err := handleWaypointDelete(ctx, request) - - assert.NoError(t, err) // MCP handlers should not return Go errors - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "istioctl waypoint delete failed") + assert.False(t, result.IsError) }) } -// Test Waypoint Status -func TestHandleWaypointStatus(t *testing.T) { - t.Run("waypoint status", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `waypoint/waypoint is deployed and ready` +func TestHandleIstioRemoteClusters(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"waypoint", "status", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddCommandString("istioctl", []string{"remote-clusters", "--timeout", "30s"}, "Remote clusters output", nil) - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "default", - } + ctx = cmd.WithShellExecutor(ctx, mock) - result, err := handleWaypointStatus(ctx, request) + result, err := handleIstioRemoteClusters(ctx, mcp.CallToolRequest{}) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "waypoint") + require.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) +} - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"waypoint", "status", "-n", "default"}, callLog[0].Args) - }) +func TestHandleWaypointList(t *testing.T) { + ctx := context.Background() - t.Run("waypoint status with specific name", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `waypoint/test-waypoint is deployed and ready` + t.Run("list waypoints in all namespaces", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"waypoint", "list", "-A", "--timeout", "30s"}, "Waypoints list", nil) - mock.AddCommandString("istioctl", []string{"waypoint", "status", "test-waypoint", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ - "namespace": "default", - "name": "test-waypoint", + "all_namespaces": "true", } - result, err := handleWaypointStatus(ctx, request) + result, err := handleWaypointList(ctx, request) - assert.NoError(t, err) + require.NoError(t, err) + assert.NotNil(t, result) assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "test-waypoint") - - // Verify the correct command was called with specific name - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"waypoint", "status", "test-waypoint", "-n", "default"}, callLog[0].Args) }) - t.Run("missing namespace parameter", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + t.Run("list waypoints in a specific namespace", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"waypoint", "list", "-n", "default", "--timeout", "30s"}, "Waypoints list", nil) - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - // Missing namespace - } - - result, err := handleWaypointStatus(ctx, request) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "namespace parameter is required") - - // Verify no commands were executed - callLog := mock.GetCallLog() - assert.Len(t, callLog, 0) - }) - - t.Run("istioctl command failure", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"waypoint", "status", "-n", "default"}, "", assert.AnError) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ "namespace": "default", } - result, err := handleWaypointStatus(ctx, request) - - assert.NoError(t, err) // MCP handlers should not return Go errors - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "istioctl waypoint status failed") - }) -} - -// Test Ztunnel Config -func TestHandleZtunnelConfig(t *testing.T) { - t.Run("default ztunnel config", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `CLUSTER_NAME CLUSTER_TYPE ENDPOINTS -cluster1 EDS 10.0.0.1:15010 -cluster2 STATIC 10.0.0.2:15010` - - mock.AddCommandString("istioctl", []string{"ztunnel-config", "all"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - result, err := handleZtunnelConfig(ctx, request) + result, err := handleWaypointList(ctx, request) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, result) assert.False(t, result.IsError) - assert.Contains(t, getResultText(result), "CLUSTER_NAME") - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"ztunnel-config", "all"}, callLog[0].Args) }) +} - t.Run("ztunnel config with namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `CLUSTER_NAME CLUSTER_TYPE ENDPOINTS -cluster1 EDS 10.0.0.1:15010` - - mock.AddCommandString("istioctl", []string{"ztunnel-config", "all", "-n", "istio-system"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "istio-system", - } - - result, err := handleZtunnelConfig(ctx, request) - - assert.NoError(t, err) - assert.False(t, result.IsError) - - // Verify the correct command was called with namespace - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"ztunnel-config", "all", "-n", "istio-system"}, callLog[0].Args) - }) +func TestHandleWaypointGenerate(t *testing.T) { + ctx := context.Background() - t.Run("ztunnel config with specific type", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `CLUSTER_NAME CLUSTER_TYPE ENDPOINTS -cluster1 EDS 10.0.0.1:15010` + t.Run("generate waypoint with namespace", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"waypoint", "generate", "waypoint", "-n", "default", "--for", "all", "--timeout", "30s"}, "Generated waypoint", nil) - mock.AddCommandString("istioctl", []string{"ztunnel-config", "cluster"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx = cmd.WithShellExecutor(ctx, mock) request := mcp.CallToolRequest{} request.Params.Arguments = map[string]interface{}{ - "config_type": "cluster", + "namespace": "default", + "name": "waypoint", + "traffic_type": "all", } - result, err := handleZtunnelConfig(ctx, request) + result, err := handleWaypointGenerate(ctx, request) - assert.NoError(t, err) + require.NoError(t, err) + assert.NotNil(t, result) assert.False(t, result.IsError) - - // Verify the correct command was called with specific config type - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"ztunnel-config", "cluster"}, callLog[0].Args) }) +} - t.Run("ztunnel config with namespace and config type", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `LISTENER_NAME ADDRESS PORT TYPE -listener1 0.0.0.0 15006 TCP` - - mock.AddCommandString("istioctl", []string{"ztunnel-config", "listener", "-n", "istio-system"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "istio-system", - "config_type": "listener", - } - - result, err := handleZtunnelConfig(ctx, request) +func TestRunIstioCtl(t *testing.T) { + t.Run("run istioctl with context", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"version", "--timeout", "30s"}, "1.18.0", nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) - assert.NoError(t, err) - assert.False(t, result.IsError) + result, err := runIstioCtl(ctx, []string{"version"}) - // Verify the correct command was called with both namespace and config type - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "istioctl", callLog[0].Command) - assert.Equal(t, []string{"ztunnel-config", "listener", "-n", "istio-system"}, callLog[0].Args) + require.NoError(t, err) + assert.Equal(t, "1.18.0", result) }) +} +func TestIstioErrorHandling(t *testing.T) { t.Run("istioctl command failure", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"ztunnel-config", "all"}, "", assert.AnError) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("istioctl", []string{"proxy-status"}, "", assert.AnError) + ctx := cmd.WithShellExecutor(context.Background(), mock) - request := mcp.CallToolRequest{} - result, err := handleZtunnelConfig(ctx, request) + result, err := handleIstioProxyStatus(ctx, mcp.CallToolRequest{}) - assert.NoError(t, err) // MCP handlers should not return Go errors + require.NoError(t, err) + assert.NotNil(t, result) assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "istioctl ztunnel-config failed") }) } diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index 6bd73ec9..6c29c9c1 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -10,12 +10,15 @@ import ( "slices" "strings" - "github.com/kagent-dev/tools/pkg/logger" - "github.com/kagent-dev/tools/pkg/utils" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/tmc/langchaingo/llms" - "github.com/tmc/langchaingo/llms/openai" + + "github.com/kagent-dev/tools/internal/cache" + "github.com/kagent-dev/tools/internal/commands" + "github.com/kagent-dev/tools/internal/logger" + "github.com/kagent-dev/tools/internal/security" + "github.com/kagent-dev/tools/internal/telemetry" ) // K8sTool struct to hold the LLM model @@ -32,6 +35,22 @@ func NewK8sToolWithConfig(kubeconfig string, llmModel llms.Model) *K8sTool { return &K8sTool{kubeconfig: kubeconfig, llmModel: llmModel} } +// runKubectlCommandWithCacheInvalidation runs a kubectl command and invalidates cache if it's a modification operation +func (k *K8sTool) runKubectlCommandWithCacheInvalidation(ctx context.Context, args ...string) (*mcp.CallToolResult, error) { + result, err := k.runKubectlCommand(ctx, args...) + + // If command succeeded and it's a modification command, invalidate cache + if err == nil && len(args) > 0 { + subcommand := args[0] + switch subcommand { + case "apply", "delete", "patch", "scale", "annotate", "label", "create", "run", "rollout": + cache.InvalidateKubernetesCache() + } + } + + return result, err +} + // Enhanced kubectl get func (k *K8sTool) handleKubectlGetEnhanced(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { resourceType := mcp.ParseString(request, "resource_type", "") @@ -62,7 +81,7 @@ func (k *K8sTool) handleKubectlGetEnhanced(ctx context.Context, request mcp.Call args = append(args, "-o", "json") } - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Get pod logs @@ -86,7 +105,7 @@ func (k *K8sTool) handleKubectlLogsEnhanced(ctx context.Context, request mcp.Cal args = append(args, "--tail", fmt.Sprintf("%d", tailLines)) } - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Scale deployment @@ -101,7 +120,7 @@ func (k *K8sTool) handleScaleDeployment(ctx context.Context, request mcp.CallToo args := []string{"scale", "deployment", deploymentName, "--replicas", fmt.Sprintf("%d", replicas), "-n", namespace} - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommandWithCacheInvalidation(ctx, args...) } // Patch resource @@ -115,9 +134,24 @@ func (k *K8sTool) handlePatchResource(ctx context.Context, request mcp.CallToolR return mcp.NewToolResultError("resource_type, resource_name, and patch parameters are required"), nil } + // Validate resource name for security + if err := security.ValidateK8sResourceName(resourceName); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid resource name: %v", err)), nil + } + + // Validate namespace for security + if err := security.ValidateNamespace(namespace); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace: %v", err)), nil + } + + // Validate patch content as JSON/YAML + if err := security.ValidateYAMLContent(patch); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid patch content: %v", err)), nil + } + args := []string{"patch", resourceType, resourceName, "-p", patch, "-n", namespace} - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommandWithCacheInvalidation(ctx, args...) } // Apply manifest from content @@ -128,18 +162,41 @@ func (k *K8sTool) handleApplyManifest(ctx context.Context, request mcp.CallToolR return mcp.NewToolResultError("manifest parameter is required"), nil } - tmpFile, err := os.CreateTemp("", "manifest-*.yaml") + // Validate YAML content for security + if err := security.ValidateYAMLContent(manifest); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid manifest content: %v", err)), nil + } + + // Create temporary file with secure permissions + tmpFile, err := os.CreateTemp("", "k8s-manifest-*.yaml") if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to create temp file: %v", err)), nil } - defer os.Remove(tmpFile.Name()) + // Ensure file is removed regardless of execution path + defer func() { + if removeErr := os.Remove(tmpFile.Name()); removeErr != nil { + logger.Get().Error("Failed to remove temporary file", "error", removeErr, "file", tmpFile.Name()) + } + }() + + // Set secure file permissions (readable/writable by owner only) + if err := os.Chmod(tmpFile.Name(), 0600); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to set file permissions: %v", err)), nil + } + + // Write manifest content to temporary file if _, err := tmpFile.WriteString(manifest); err != nil { + tmpFile.Close() return mcp.NewToolResultError(fmt.Sprintf("Failed to write to temp file: %v", err)), nil } - tmpFile.Close() - return k.runKubectlCommand(ctx, []string{"apply", "-f", tmpFile.Name()}) + // Close the file before passing to kubectl + if err := tmpFile.Close(); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to close temp file: %v", err)), nil + } + + return k.runKubectlCommandWithCacheInvalidation(ctx, "apply", "-f", tmpFile.Name()) } // Delete resource @@ -154,7 +211,7 @@ func (k *K8sTool) handleDeleteResource(ctx context.Context, request mcp.CallTool args := []string{"delete", resourceType, resourceName, "-n", namespace} - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommandWithCacheInvalidation(ctx, args...) } // Check service connectivity @@ -169,23 +226,23 @@ func (k *K8sTool) handleCheckServiceConnectivity(ctx context.Context, request mc // Create a temporary curl pod for connectivity check podName := fmt.Sprintf("curl-test-%d", rand.Intn(10000)) defer func() { - _, _ = k.runKubectlCommand(ctx, []string{"delete", "pod", podName, "-n", namespace, "--ignore-not-found"}) + _, _ = k.runKubectlCommand(ctx, "delete", "pod", podName, "-n", namespace, "--ignore-not-found") }() // Create the curl pod - _, err := k.runKubectlCommand(ctx, []string{"run", podName, "--image=curlimages/curl", "-n", namespace, "--restart=Never", "--", "sleep", "3600"}) + _, err := k.runKubectlCommand(ctx, "run", podName, "--image=curlimages/curl", "-n", namespace, "--restart=Never", "--", "sleep", "3600") if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to create curl pod: %v", err)), nil } // Wait for pod to be ready - _, err = k.runKubectlCommand(ctx, []string{"wait", "--for=condition=ready", "pod/" + podName, "-n", namespace, "--timeout=60s"}) + _, err = k.runKubectlCommand(ctx, "wait", "--for=condition=ready", "pod/"+podName, "-n", namespace, "--timeout=60s") if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to wait for curl pod: %v", err)), nil } - // Execute curl command - return k.runKubectlCommand(ctx, []string{"exec", podName, "-n", namespace, "--", "curl", "-s", serviceName}) + // Execute kubectl command + return k.runKubectlCommand(ctx, "exec", podName, "-n", namespace, "--", "curl", "-s", serviceName) } // Get cluster events @@ -199,7 +256,7 @@ func (k *K8sTool) handleGetEvents(ctx context.Context, request mcp.CallToolReque args = append(args, "--all-namespaces") } - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Execute command in pod @@ -212,14 +269,29 @@ func (k *K8sTool) handleExecCommand(ctx context.Context, request mcp.CallToolReq return mcp.NewToolResultError("pod_name and command parameters are required"), nil } + // Validate pod name for security + if err := security.ValidateK8sResourceName(podName); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid pod name: %v", err)), nil + } + + // Validate namespace for security + if err := security.ValidateNamespace(namespace); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace: %v", err)), nil + } + + // Validate command input for security + if err := security.ValidateCommandInput(command); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid command: %v", err)), nil + } + args := []string{"exec", podName, "-n", namespace, "--", command} - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Get available API resources func (k *K8sTool) handleGetAvailableAPIResources(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return k.runKubectlCommand(ctx, []string{"api-resources", "-o", "json"}) + return k.runKubectlCommand(ctx, "api-resources", "-o", "json") } // Kubectl describe tool @@ -237,7 +309,7 @@ func (k *K8sTool) handleKubectlDescribeTool(ctx context.Context, request mcp.Cal args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Rollout operations @@ -256,12 +328,12 @@ func (k *K8sTool) handleRollout(ctx context.Context, request mcp.CallToolRequest args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Get cluster configuration func (k *K8sTool) handleGetClusterConfiguration(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return k.runKubectlCommand(ctx, []string{"config", "view"}) + return k.runKubectlCommand(ctx, "config", "view", "-o", "json") } // Remove annotation @@ -280,7 +352,7 @@ func (k *K8sTool) handleRemoveAnnotation(ctx context.Context, request mcp.CallTo args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Remove label @@ -299,7 +371,7 @@ func (k *K8sTool) handleRemoveLabel(ctx context.Context, request mcp.CallToolReq args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Annotate resource @@ -320,7 +392,7 @@ func (k *K8sTool) handleAnnotateResource(ctx context.Context, request mcp.CallTo args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Label resource @@ -341,7 +413,7 @@ func (k *K8sTool) handleLabelResource(ctx context.Context, request mcp.CallToolR args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Create resource from URL @@ -358,7 +430,7 @@ func (k *K8sTool) handleCreateResourceFromURL(ctx context.Context, request mcp.C args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args) + return k.runKubectlCommand(ctx, args...) } // Resource generation embeddings @@ -450,30 +522,27 @@ func (k *K8sTool) handleGenerateResource(ctx context.Context, request mcp.CallTo return mcp.NewToolResultError("empty response from model"), nil } c1 := choices[0] - return mcp.NewToolResultText(c1.Content), nil + responseText := c1.Content + + return mcp.NewToolResultText(responseText), nil } -// Helper function to run kubectl commands -func (k *K8sTool) runKubectlCommand(ctx context.Context, args []string) (*mcp.CallToolResult, error) { - if k.kubeconfig != "" { - args = append([]string{"--kubeconfig", k.kubeconfig}, args...) - } - result, err := utils.RunCommandWithContext(ctx, "kubectl", args) +// runKubectlCommand is a helper function to execute kubectl commands +func (k *K8sTool) runKubectlCommand(ctx context.Context, args ...string) (*mcp.CallToolResult, error) { + output, err := commands.NewCommandBuilder("kubectl"). + WithArgs(args...). + WithKubeconfig(k.kubeconfig). + Execute(ctx) + if err != nil { return mcp.NewToolResultError(err.Error()), nil } - return mcp.NewToolResultText(result), nil + + return mcp.NewToolResultText(output), nil } // RegisterK8sTools registers all k8s tools with the MCP server -func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { - var llm llms.Model - if openAiClient, err := openai.New(); err == nil { - llm = openAiClient - } else { - logger.Get().Error(err, "Failed to initialize OpenAI LLM, k8s_generate_resource tool will not be available") - } - +func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { k8sTool := NewK8sToolWithConfig(kubeconfig, llm) s.AddTool(mcp.NewTool("k8s_get_resources", @@ -483,7 +552,7 @@ func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("namespace", mcp.Description("Namespace to query (optional)")), mcp.WithString("all_namespaces", mcp.Description("Query all namespaces (true/false)")), mcp.WithString("output", mcp.Description("Output format (json, yaml, wide, etc.)")), - ), k8sTool.handleKubectlGetEnhanced) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_resources", k8sTool.handleKubectlGetEnhanced))) s.AddTool(mcp.NewTool("k8s_get_pod_logs", mcp.WithDescription("Get logs from a Kubernetes pod"), @@ -491,14 +560,14 @@ func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("namespace", mcp.Description("Namespace of the pod (default: default)")), mcp.WithString("container", mcp.Description("Container name (for multi-container pods)")), mcp.WithNumber("tail_lines", mcp.Description("Number of lines to show from the end (default: 50)")), - ), k8sTool.handleKubectlLogsEnhanced) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_pod_logs", k8sTool.handleKubectlLogsEnhanced))) s.AddTool(mcp.NewTool("k8s_scale", mcp.WithDescription("Scale a Kubernetes deployment"), mcp.WithString("name", mcp.Description("Name of the deployment"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace of the deployment (default: default)")), mcp.WithNumber("replicas", mcp.Description("Number of replicas"), mcp.Required()), - ), k8sTool.handleScaleDeployment) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_scale", k8sTool.handleScaleDeployment))) s.AddTool(mcp.NewTool("k8s_patch_resource", mcp.WithDescription("Patch a Kubernetes resource using strategic merge patch"), @@ -506,45 +575,46 @@ func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), mcp.WithString("patch", mcp.Description("JSON patch to apply"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), - ), k8sTool.handlePatchResource) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_patch_resource", k8sTool.handlePatchResource))) s.AddTool(mcp.NewTool("k8s_apply_manifest", mcp.WithDescription("Apply a YAML manifest to the Kubernetes cluster"), mcp.WithString("manifest", mcp.Description("YAML manifest content"), mcp.Required()), - ), k8sTool.handleApplyManifest) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_apply_manifest", k8sTool.handleApplyManifest))) s.AddTool(mcp.NewTool("k8s_delete_resource", mcp.WithDescription("Delete a Kubernetes resource"), mcp.WithString("resource_type", mcp.Description("Type of resource (pod, service, deployment, etc.)"), mcp.Required()), mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), - ), k8sTool.handleDeleteResource) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_delete_resource", k8sTool.handleDeleteResource))) s.AddTool(mcp.NewTool("k8s_check_service_connectivity", mcp.WithDescription("Check connectivity to a service using a temporary curl pod"), mcp.WithString("service_name", mcp.Description("Service name to test (e.g., my-service.my-namespace.svc.cluster.local:80)"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace to run the check from (default: default)")), - ), k8sTool.handleCheckServiceConnectivity) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_check_service_connectivity", k8sTool.handleCheckServiceConnectivity))) s.AddTool(mcp.NewTool("k8s_get_events", - mcp.WithDescription("Get Kubernetes cluster events"), - mcp.WithString("namespace", mcp.Description("Namespace to query events from (optional, default: all namespaces)")), - ), k8sTool.handleGetEvents) + mcp.WithDescription("Get events from a Kubernetes namespace"), + mcp.WithString("namespace", mcp.Description("Namespace to get events from (default: default)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_events", k8sTool.handleGetEvents))) s.AddTool(mcp.NewTool("k8s_execute_command", - mcp.WithDescription("Execute a command inside a Kubernetes pod"), - mcp.WithString("pod_name", mcp.Description("Name of the pod"), mcp.Required()), + mcp.WithDescription("Execute a command in a Kubernetes pod"), + mcp.WithString("pod_name", mcp.Description("Name of the pod to execute in"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace of the pod (default: default)")), + mcp.WithString("container", mcp.Description("Container name (for multi-container pods)")), mcp.WithString("command", mcp.Description("Command to execute"), mcp.Required()), - ), k8sTool.handleExecCommand) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_execute_command", k8sTool.handleExecCommand))) s.AddTool(mcp.NewTool("k8s_get_available_api_resources", - mcp.WithDescription("Get all available API resources from the Kubernetes cluster"), - ), k8sTool.handleGetAvailableAPIResources) + mcp.WithDescription("Get available Kubernetes API resources"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_available_api_resources", k8sTool.handleGetAvailableAPIResources))) s.AddTool(mcp.NewTool("k8s_get_cluster_configuration", - mcp.WithDescription("Get the current kubectl cluster configuration"), - ), k8sTool.handleGetClusterConfiguration) + mcp.WithDescription("Get cluster configuration details"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_cluster_configuration", k8sTool.handleGetClusterConfiguration))) s.AddTool(mcp.NewTool("k8s_rollout", mcp.WithDescription("Perform rollout operations on Kubernetes resources (history, pause, restart, resume, status, undo)"), @@ -552,7 +622,7 @@ func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("resource_type", mcp.Description("The type of resource to rollout (e.g., deployment)"), mcp.Required()), mcp.WithString("resource_name", mcp.Description("The name of the resource to rollout"), mcp.Required()), mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), k8sTool.handleRollout) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_rollout", k8sTool.handleRollout))) s.AddTool(mcp.NewTool("k8s_label_resource", mcp.WithDescription("Add or update labels on a Kubernetes resource"), @@ -560,7 +630,7 @@ func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), mcp.WithString("labels", mcp.Description("Space-separated key=value pairs for labels"), mcp.Required()), mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), k8sTool.handleLabelResource) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_label_resource", k8sTool.handleLabelResource))) s.AddTool(mcp.NewTool("k8s_annotate_resource", mcp.WithDescription("Add or update annotations on a Kubernetes resource"), @@ -568,7 +638,7 @@ func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), mcp.WithString("annotations", mcp.Description("Space-separated key=value pairs for annotations"), mcp.Required()), mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), k8sTool.handleAnnotateResource) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_annotate_resource", k8sTool.handleAnnotateResource))) s.AddTool(mcp.NewTool("k8s_remove_annotation", mcp.WithDescription("Remove an annotation from a Kubernetes resource"), @@ -576,7 +646,7 @@ func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), mcp.WithString("annotation_key", mcp.Description("The key of the annotation to remove"), mcp.Required()), mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), k8sTool.handleRemoveAnnotation) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_remove_annotation", k8sTool.handleRemoveAnnotation))) s.AddTool(mcp.NewTool("k8s_remove_label", mcp.WithDescription("Remove a label from a Kubernetes resource"), @@ -584,12 +654,12 @@ func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), mcp.WithString("label_key", mcp.Description("The key of the label to remove"), mcp.Required()), mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), k8sTool.handleRemoveLabel) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_remove_label", k8sTool.handleRemoveLabel))) s.AddTool(mcp.NewTool("k8s_create_resource", mcp.WithDescription("Create a Kubernetes resource from YAML content"), mcp.WithString("yaml_content", mcp.Description("YAML content of the resource"), mcp.Required()), - ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_create_resource", func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { yamlContent := mcp.ParseString(request, "yaml_content", "") if yamlContent == "" { @@ -608,26 +678,26 @@ func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { } tmpFile.Close() - result, err := utils.RunCommandWithContext(ctx, "kubectl", []string{"create", "-f", tmpFile.Name()}) + result, err := k8sTool.runKubectlCommand(ctx, "create", "-f", tmpFile.Name()) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Create command failed: %v", err)), nil } - return mcp.NewToolResultText(result), nil - }) + return result, nil + }))) s.AddTool(mcp.NewTool("k8s_create_resource_from_url", mcp.WithDescription("Create a Kubernetes resource from a URL pointing to a YAML manifest"), mcp.WithString("url", mcp.Description("The URL of the manifest"), mcp.Required()), mcp.WithString("namespace", mcp.Description("The namespace to create the resource in")), - ), k8sTool.handleCreateResourceFromURL) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_create_resource_from_url", k8sTool.handleCreateResourceFromURL))) s.AddTool(mcp.NewTool("k8s_get_resource_yaml", mcp.WithDescription("Get the YAML representation of a Kubernetes resource"), mcp.WithString("resource_type", mcp.Description("Type of resource"), mcp.Required()), mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace of the resource (optional)")), - ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_resource_yaml", func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { resourceType := mcp.ParseString(request, "resource_type", "") resourceName := mcp.ParseString(request, "resource_name", "") namespace := mcp.ParseString(request, "namespace", "") @@ -641,24 +711,24 @@ func RegisterK8sTools(s *server.MCPServer, kubeconfig string) { args = append(args, "-n", namespace) } - result, err := utils.RunCommandWithContext(ctx, "kubectl", args) + result, err := k8sTool.runKubectlCommand(ctx, args...) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Get YAML command failed: %v", err)), nil } - return mcp.NewToolResultText(result), nil - }) + return result, nil + }))) s.AddTool(mcp.NewTool("k8s_describe_resource", mcp.WithDescription("Describe a Kubernetes resource in detail"), mcp.WithString("resource_type", mcp.Description("Type of resource (deployment, service, pod, node, etc.)"), mcp.Required()), mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), mcp.WithString("namespace", mcp.Description("Namespace of the resource (optional)")), - ), k8sTool.handleKubectlDescribeTool) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_describe_resource", k8sTool.handleKubectlDescribeTool))) s.AddTool(mcp.NewTool("k8s_generate_resource", mcp.WithDescription("Generate a Kubernetes resource YAML from a description"), mcp.WithString("resource_description", mcp.Description("Detailed description of the resource to generate"), mcp.Required()), mcp.WithString("resource_type", mcp.Description(fmt.Sprintf("Type of resource to generate (%s)", strings.Join(slices.Collect(resourceTypes), ", "))), mcp.Required()), - ), k8sTool.handleGenerateResource) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_generate_resource", k8sTool.handleGenerateResource))) } diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go index 240e7aca..a71e10fb 100644 --- a/pkg/k8s/k8s_test.go +++ b/pkg/k8s/k8s_test.go @@ -2,11 +2,9 @@ package k8s import ( "context" - "fmt" - "os" "testing" - "github.com/kagent-dev/tools/pkg/utils" + "github.com/kagent-dev/tools/internal/cmd" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -38,10 +36,10 @@ func TestHandleGetAvailableAPIResources(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `[{"name": "pods", "singularName": "pod", "namespaced": true, "kind": "Pod"}]` mock.AddCommandString("kubectl", []string{"api-resources", "-o", "json"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -57,9 +55,9 @@ func TestHandleGetAvailableAPIResources(t *testing.T) { }) t.Run("kubectl command failure", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() mock.AddCommandString("kubectl", []string{"api-resources", "-o", "json"}, "", assert.AnError) - ctx := utils.WithShellExecutor(ctx, mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -75,10 +73,10 @@ func TestHandleScaleDeployment(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment scaled` mock.AddCommandString("kubectl", []string{"scale", "deployment", "test-deployment", "--replicas", "5", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -99,8 +97,8 @@ func TestHandleScaleDeployment(t *testing.T) { }) t.Run("missing name parameter", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -122,18 +120,16 @@ func TestHandleScaleDeployment(t *testing.T) { }) t.Run("missing replicas parameter uses default", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment scaled` - // Default replicas is 1 mock.AddCommandString("kubectl", []string{"scale", "deployment", "test-deployment", "--replicas", "1", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ "name": "test-deployment", - // Missing replicas parameter - should use default value of 1 } result, err := k8sTool.handleScaleDeployment(ctx, req) @@ -156,10 +152,10 @@ func TestHandleGetEvents(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `{"items": [{"metadata": {"name": "test-event"}, "message": "Test event message"}]}` mock.AddCommandString("kubectl", []string{"get", "events", "-o", "json", "--all-namespaces"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -174,10 +170,10 @@ func TestHandleGetEvents(t *testing.T) { }) t.Run("with namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `{"items": []}` mock.AddCommandString("kubectl", []string{"get", "events", "-o", "json", "-n", "custom-namespace"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -197,8 +193,8 @@ func TestHandlePatchResource(t *testing.T) { ctx := context.Background() t.Run("missing parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -219,10 +215,10 @@ func TestHandlePatchResource(t *testing.T) { }) t.Run("valid parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment patched` mock.AddCommandString("kubectl", []string{"patch", "deployment", "test-deployment", "-p", `{"spec":{"replicas":5}}`, "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -247,8 +243,8 @@ func TestHandleDeleteResource(t *testing.T) { ctx := context.Background() t.Run("missing parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -269,17 +265,17 @@ func TestHandleDeleteResource(t *testing.T) { }) t.Run("valid parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `pod "test-pod" deleted` - mock.AddCommandString("kubectl", []string{"delete", "pod", "test-pod", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment deleted` + mock.AddCommandString("kubectl", []string{"delete", "deployment", "test-deployment", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} req.Params.Arguments = map[string]interface{}{ - "resource_type": "pod", - "resource_name": "test-pod", + "resource_type": "deployment", + "resource_name": "test-deployment", } result, err := k8sTool.handleDeleteResource(ctx, req) @@ -296,8 +292,8 @@ func TestHandleCheckServiceConnectivity(t *testing.T) { ctx := context.Background() t.Run("missing service_name", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -315,15 +311,15 @@ func TestHandleCheckServiceConnectivity(t *testing.T) { }) t.Run("valid service_name", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() // Mock the pod creation, wait, and exec commands using partial matchers mock.AddPartialMatcherString("kubectl", []string{"run", "*", "--image=curlimages/curl", "-n", "default", "--restart=Never", "--", "sleep", "3600"}, "pod/curl-test-123 created", nil) - mock.AddPartialMatcherString("kubectl", []string{"wait", "--for=condition=ready", "*", "-n", "default", "--timeout=60s"}, "pod/curl-test-123 condition met", nil) + mock.AddPartialMatcherString("kubectl", []string{"wait", "--for=condition=ready", "*", "-n", "default", "--timeout=60s", "--timeout", "30s"}, "pod/curl-test-123 condition met", nil) mock.AddPartialMatcherString("kubectl", []string{"exec", "*", "-n", "default", "--", "curl", "-s", "test-service.default.svc.cluster.local:80"}, "Connection successful", nil) - mock.AddPartialMatcherString("kubectl", []string{"delete", "pod", "*", "-n", "default", "--ignore-not-found"}, "pod deleted", nil) + mock.AddPartialMatcherString("kubectl", []string{"delete", "pod", "*", "-n", "default", "--ignore-not-found", "--timeout", "30s"}, "pod deleted", nil) - ctx := utils.WithShellExecutor(ctx, mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -343,8 +339,8 @@ func TestHandleKubectlDescribeTool(t *testing.T) { ctx := context.Background() t.Run("missing parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -365,12 +361,12 @@ func TestHandleKubectlDescribeTool(t *testing.T) { }) t.Run("valid parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `Name: test-deployment Namespace: default Labels: app=test` mock.AddCommandString("kubectl", []string{"describe", "deployment", "test-deployment", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -395,8 +391,8 @@ func TestHandleKubectlGetEnhanced(t *testing.T) { ctx := context.Background() t.Run("missing resource_type", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} @@ -411,10 +407,10 @@ func TestHandleKubectlGetEnhanced(t *testing.T) { }) t.Run("valid resource_type", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `{"items": [{"metadata": {"name": "pod1"}}]}` mock.AddCommandString("kubectl", []string{"get", "pods", "-o", "json"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} @@ -430,8 +426,8 @@ func TestHandleKubectlLogsEnhanced(t *testing.T) { ctx := context.Background() t.Run("missing pod_name", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} @@ -446,11 +442,11 @@ func TestHandleKubectlLogsEnhanced(t *testing.T) { }) t.Run("valid pod_name", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `log line 1 log line 2` mock.AddCommandString("kubectl", []string{"logs", "test-pod", "-n", "default", "--tail", "50"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() req := mcp.CallToolRequest{} @@ -463,8 +459,9 @@ log line 2` } func TestHandleApplyManifest(t *testing.T) { + ctx := context.Background() t.Run("apply manifest from string", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() manifest := `apiVersion: v1 kind: Pod metadata: @@ -476,8 +473,8 @@ spec: expectedOutput := `pod/test-pod created` // Use partial matcher to handle dynamic temp file names - mock.AddPartialMatcherString("kubectl", []string{"apply", "-f", "*"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + mock.AddPartialMatcherString("kubectl", []string{"apply", "-f"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -507,8 +504,8 @@ spec: }) t.Run("missing manifest parameter", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -530,15 +527,16 @@ spec: } func TestHandleExecCommand(t *testing.T) { + ctx := context.Background() t.Run("exec command in pod", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `total 8 drwxr-xr-x 1 root root 4096 Jan 1 12:00 . drwxr-xr-x 1 root root 4096 Jan 1 12:00 ..` // The implementation passes the command as a single string after -- mock.AddCommandString("kubectl", []string{"exec", "mypod", "-n", "default", "--", "ls -la"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -566,8 +564,8 @@ drwxr-xr-x 1 root root 4096 Jan 1 12:00 ..` }) t.Run("missing required parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -590,12 +588,13 @@ drwxr-xr-x 1 root root 4096 Jan 1 12:00 ..` } func TestHandleRollout(t *testing.T) { + ctx := context.Background() t.Run("rollout restart deployment", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/myapp restarted` mock.AddCommandString("kubectl", []string{"rollout", "restart", "deployment/myapp", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(context.Background(), mock) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -624,8 +623,8 @@ func TestHandleRollout(t *testing.T) { }) t.Run("missing required parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -773,10 +772,10 @@ func TestHandleAnnotateResource(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment annotated` - mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1=value1", "key2=value2", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1=value1", "key2=value2", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -798,8 +797,8 @@ func TestHandleAnnotateResource(t *testing.T) { }) t.Run("missing parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -825,10 +824,10 @@ func TestHandleLabelResource(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment labeled` - mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env=prod", "version=1.0", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env=prod", "version=1.0", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -850,8 +849,8 @@ func TestHandleLabelResource(t *testing.T) { }) t.Run("missing parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -877,10 +876,10 @@ func TestHandleRemoveAnnotation(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment annotated` - mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1-", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1-", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -902,8 +901,8 @@ func TestHandleRemoveAnnotation(t *testing.T) { }) t.Run("missing parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -929,10 +928,10 @@ func TestHandleRemoveLabel(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment labeled` - mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env-", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env-", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -954,8 +953,8 @@ func TestHandleRemoveLabel(t *testing.T) { }) t.Run("missing parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -981,10 +980,10 @@ func TestHandleCreateResourceFromURL(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment created` - mock.AddCommandString("kubectl", []string{"create", "-f", "https://example.com/manifest.yaml", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + mock.AddCommandString("kubectl", []string{"create", "-f", "https://example.com/manifest.yaml", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -1004,8 +1003,8 @@ func TestHandleCreateResourceFromURL(t *testing.T) { }) t.Run("missing url parameter", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) k8sTool := newTestK8sTool() @@ -1030,7 +1029,7 @@ func TestHandleGetClusterConfiguration(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() + mock := cmd.NewMockShellExecutor() expectedOutput := `apiVersion: v1 clusters: - cluster: @@ -1046,8 +1045,8 @@ kind: Config preferences: {} users: - name: default` - mock.AddCommandString("kubectl", []string{"config", "view"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) + mock.AddCommandString("kubectl", []string{"config", "view", "-o", "json"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -1062,258 +1061,3 @@ users: assert.Contains(t, resultText, "clusters") }) } - -// Test the k8s_create_resource handler (inline function in RegisterK8sTools) -func TestHandleCreateResource(t *testing.T) { - ctx := context.Background() - - t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - yamlContent := `apiVersion: v1 -kind: Pod -metadata: - name: test-pod -spec: - containers: - - name: test - image: nginx` - - expectedOutput := `pod/test-pod created` - // Use partial matcher to handle dynamic temp file names - mock.AddPartialMatcherString("kubectl", []string{"create", "-f", "*"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) - - // We need to test the inline function from RegisterK8sTools - // Let's create a test handler that mimics the inline function - testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - yamlContent := mcp.ParseString(request, "yaml_content", "") - - if yamlContent == "" { - return mcp.NewToolResultError("yaml_content is required"), nil - } - - // Create temporary file - tmpFile, err := os.CreateTemp("", "k8s-resource-*.yaml") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create temp file: %v", err)), nil - } - defer os.Remove(tmpFile.Name()) - - if _, err := tmpFile.WriteString(yamlContent); err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to write to temp file: %v", err)), nil - } - tmpFile.Close() - - result, err := utils.RunCommandWithContext(ctx, "kubectl", []string{"create", "-f", tmpFile.Name()}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Create command failed: %v", err)), nil - } - - return mcp.NewToolResultText(result), nil - } - - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "yaml_content": yamlContent, - } - - result, err := testHandler(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.False(t, result.IsError) - - // Verify the expected output - content := getResultText(result) - assert.Contains(t, content, "created") - - // Verify kubectl create was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "kubectl", callLog[0].Command) - assert.Len(t, callLog[0].Args, 3) // create, -f, - assert.Equal(t, "create", callLog[0].Args[0]) - assert.Equal(t, "-f", callLog[0].Args[1]) - // Third argument should be the temporary file path - assert.Contains(t, callLog[0].Args[2], "k8s-resource-") - }) - - t.Run("missing yaml_content parameter", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) - - // Test handler for missing parameter - testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - yamlContent := mcp.ParseString(request, "yaml_content", "") - - if yamlContent == "" { - return mcp.NewToolResultError("yaml_content is required"), nil - } - return mcp.NewToolResultText("should not reach here"), nil - } - - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - // Missing yaml_content parameter - } - - result, err := testHandler(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "yaml_content is required") - - // Verify no commands were executed - callLog := mock.GetCallLog() - assert.Len(t, callLog, 0) - }) -} - -// Test the k8s_get_resource_yaml handler (inline function in RegisterK8sTools) -func TestHandleGetResourceYAML(t *testing.T) { - ctx := context.Background() - - t.Run("success", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `apiVersion: v1 -kind: Pod -metadata: - name: test-pod - namespace: default -spec: - containers: - - name: test - image: nginx` - mock.AddCommandString("kubectl", []string{"get", "pod", "test-pod", "-o", "yaml", "-n", "default"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) - - // Test handler that mimics the inline function - testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - resourceType := mcp.ParseString(request, "resource_type", "") - resourceName := mcp.ParseString(request, "resource_name", "") - namespace := mcp.ParseString(request, "namespace", "") - - if resourceType == "" || resourceName == "" { - return mcp.NewToolResultError("resource_type and resource_name are required"), nil - } - - args := []string{"get", resourceType, resourceName, "-o", "yaml"} - if namespace != "" { - args = append(args, "-n", namespace) - } - - result, err := utils.RunCommandWithContext(ctx, "kubectl", args) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Get YAML command failed: %v", err)), nil - } - - return mcp.NewToolResultText(result), nil - } - - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "resource_type": "pod", - "resource_name": "test-pod", - "namespace": "default", - } - - result, err := testHandler(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.False(t, result.IsError) - - resultText := getResultText(result) - assert.Contains(t, resultText, "test-pod") - assert.Contains(t, resultText, "apiVersion") - - // Verify the correct kubectl command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"get", "pod", "test-pod", "-o", "yaml", "-n", "default"}, callLog[0].Args) - }) - - t.Run("missing parameters", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - ctx := utils.WithShellExecutor(context.Background(), mock) - - testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - resourceType := mcp.ParseString(request, "resource_type", "") - resourceName := mcp.ParseString(request, "resource_name", "") - - if resourceType == "" || resourceName == "" { - return mcp.NewToolResultError("resource_type and resource_name are required"), nil - } - return mcp.NewToolResultText("should not reach here"), nil - } - - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "resource_type": "pod", - // Missing resource_name - } - - result, err := testHandler(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "resource_type and resource_name are required") - - // Verify no commands were executed - callLog := mock.GetCallLog() - assert.Len(t, callLog, 0) - }) - - t.Run("without namespace", func(t *testing.T) { - mock := utils.NewMockShellExecutor() - expectedOutput := `apiVersion: v1 -kind: ClusterRole -metadata: - name: test-cluster-role` - mock.AddCommandString("kubectl", []string{"get", "clusterrole", "test-cluster-role", "-o", "yaml"}, expectedOutput, nil) - ctx := utils.WithShellExecutor(ctx, mock) - - testHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - resourceType := mcp.ParseString(request, "resource_type", "") - resourceName := mcp.ParseString(request, "resource_name", "") - namespace := mcp.ParseString(request, "namespace", "") - - if resourceType == "" || resourceName == "" { - return mcp.NewToolResultError("resource_type and resource_name are required"), nil - } - - args := []string{"get", resourceType, resourceName, "-o", "yaml"} - if namespace != "" { - args = append(args, "-n", namespace) - } - - result, err := utils.RunCommandWithContext(ctx, "kubectl", args) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Get YAML command failed: %v", err)), nil - } - - return mcp.NewToolResultText(result), nil - } - - req := mcp.CallToolRequest{} - req.Params.Arguments = map[string]interface{}{ - "resource_type": "clusterrole", - "resource_name": "test-cluster-role", - // No namespace for cluster-scoped resource - } - - result, err := testHandler(ctx, req) - assert.NoError(t, err) - assert.NotNil(t, result) - assert.False(t, result.IsError) - - resultText := getResultText(result) - assert.Contains(t, resultText, "test-cluster-role") - assert.Contains(t, resultText, "ClusterRole") - - // Verify the correct kubectl command was called (without namespace) - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"get", "clusterrole", "test-cluster-role", "-o", "yaml"}, callLog[0].Args) - }) -} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go deleted file mode 100644 index 062997de..00000000 --- a/pkg/logger/logger.go +++ /dev/null @@ -1,58 +0,0 @@ -package logger - -import ( - "github.com/go-logr/logr" - "github.com/go-logr/stdr" -) - -var globalLogger logr.Logger - -// Init initializes the global logger with appropriate configuration -func Init() { - // Set log level from environment variable (not directly supported by stdr, but can be extended) - // For now, just use stdr with default settings - globalLogger = stdr.New(nil) -} - -// Get returns the global logger instance -func Get() logr.Logger { - if globalLogger.GetSink() == nil { - Init() - } - return globalLogger -} - -// LogExecCommand logs information about an exec command being executed -func LogExecCommand(command string, args []string, caller string) { - logger := Get() - logger.Info("executing command", - "command", command, - "args", args, - "caller", caller, - ) -} - -// LogExecCommandResult logs the result of an exec command -func LogExecCommandResult(command string, args []string, output string, err error, duration float64, caller string) { - logger := Get() - - if err != nil { - logger.Error(err, "command execution failed", - "command", command, - "args", args, - "duration_seconds", duration, - "caller", caller, - ) - } else { - logger.Info("command execution successful", - "command", command, - "args", args, - "output", output, - "duration_seconds", duration, - "caller", caller, - ) - } -} - -// Sync is a no-op for logr/stdr -func Sync() {} diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go deleted file mode 100644 index 24903d07..00000000 --- a/pkg/logger/logger_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package logger - -import ( - "os" - "testing" - - "github.com/go-logr/logr" - "github.com/stretchr/testify/assert" -) - -func TestInit(t *testing.T) { - // Test initialization - Init() - assert.NotNil(t, globalLogger) -} - -func TestGet(t *testing.T) { - // Reset global logger - globalLogger = logr.Logger{} - - // Test Get without Init - logger := Get() - assert.NotNil(t, logger) - assert.NotNil(t, globalLogger) -} - -func TestLogExecCommand(t *testing.T) { - // Just test that it does not panic and logs - assert.NotPanics(t, func() { - LogExecCommand("test-command", []string{"arg1", "arg2"}, "test.go:123") - }) -} - -func TestLogExecCommandResult(t *testing.T) { - // Test successful command - assert.NotPanics(t, func() { - LogExecCommandResult("test-command", []string{"arg1"}, "success output", nil, 1.5, "test.go:123") - }) - // Test failed command - assert.NotPanics(t, func() { - LogExecCommandResult("test-command", []string{"arg1"}, "error output", assert.AnError, 0.5, "test.go:123") - }) -} - -func TestEnvironmentVariables(t *testing.T) { - // Test log level from environment (no-op for stdr) - os.Setenv("KAGENT_LOG_LEVEL", "debug") - defer os.Unsetenv("KAGENT_LOG_LEVEL") - - // Reset global logger - globalLogger = logr.Logger{} - - // Initialize with environment variable - Init() - - // Just check logger is set - assert.NotNil(t, globalLogger) -} - -func TestDevelopmentMode(t *testing.T) { - // Test development mode (no-op for stdr) - os.Setenv("KAGENT_ENV", "development") - defer os.Unsetenv("KAGENT_ENV") - - // Reset global logger - globalLogger = logr.Logger{} - - // Initialize in development mode - Init() - - // In development mode, the logger should be configured (no panic) - assert.NotNil(t, globalLogger) -} - -func TestSync(t *testing.T) { - // Test Sync function - Init() - - // Sync should not panic - assert.NotPanics(t, func() { - Sync() - }) -} diff --git a/pkg/prometheus/prometheus.go b/pkg/prometheus/prometheus.go index dc4d6cb8..1239305f 100644 --- a/pkg/prometheus/prometheus.go +++ b/pkg/prometheus/prometheus.go @@ -9,6 +9,9 @@ import ( "net/url" "time" + "github.com/kagent-dev/tools/internal/errors" + "github.com/kagent-dev/tools/internal/security" + "github.com/kagent-dev/tools/internal/telemetry" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) @@ -33,6 +36,16 @@ func handlePrometheusQueryTool(ctx context.Context, request mcp.CallToolRequest) return mcp.NewToolResultError("query parameter is required"), nil } + // Validate prometheus URL + if err := security.ValidateURL(prometheusURL); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid Prometheus URL: %v", err)), nil + } + + // Validate PromQL query + if err := security.ValidatePromQLQuery(query); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid PromQL query: %v", err)), nil + } + // Make request to Prometheus API apiURL := fmt.Sprintf("%s/api/v1/query", prometheusURL) params := url.Values{} @@ -42,19 +55,40 @@ func handlePrometheusQueryTool(ctx context.Context, request mcp.CallToolRequest) fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode()) client := getHTTPClient(ctx) - resp, err := client.Get(fullURL) + req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil) if err != nil { - return mcp.NewToolResultError("failed to query Prometheus: " + err.Error()), nil + toolErr := errors.NewPrometheusError("create_request", err). + WithContext("prometheus_url", prometheusURL). + WithContext("query", query) + return toolErr.ToMCPResult(), nil + } + + resp, err := client.Do(req) + if err != nil { + toolErr := errors.NewPrometheusError("query_execution", err). + WithContext("prometheus_url", prometheusURL). + WithContext("query", query). + WithContext("api_url", apiURL) + return toolErr.ToMCPResult(), nil } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return mcp.NewToolResultError("failed to read response: " + err.Error()), nil + toolErr := errors.NewPrometheusError("read_response", err). + WithContext("prometheus_url", prometheusURL). + WithContext("query", query). + WithContext("status_code", resp.StatusCode) + return toolErr.ToMCPResult(), nil } if resp.StatusCode != http.StatusOK { - return mcp.NewToolResultError(fmt.Sprintf("Prometheus API error (%d): %s", resp.StatusCode, string(body))), nil + toolErr := errors.NewPrometheusError("api_error", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))). + WithContext("prometheus_url", prometheusURL). + WithContext("query", query). + WithContext("status_code", resp.StatusCode). + WithContext("response_body", string(body)) + return toolErr.ToMCPResult(), nil } // Parse the JSON response to pretty-print it @@ -82,6 +116,33 @@ func handlePrometheusRangeQueryTool(ctx context.Context, request mcp.CallToolReq return mcp.NewToolResultError("query parameter is required"), nil } + // Validate prometheus URL + if err := security.ValidateURL(prometheusURL); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid Prometheus URL: %v", err)), nil + } + + // Validate PromQL query + if err := security.ValidatePromQLQuery(query); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid PromQL query: %v", err)), nil + } + + // Validate time parameters if provided + if start != "" { + if err := security.ValidateCommandInput(start); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid start time: %v", err)), nil + } + } + if end != "" { + if err := security.ValidateCommandInput(end); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid end time: %v", err)), nil + } + } + if step != "" { + if err := security.ValidateCommandInput(step); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid step parameter: %v", err)), nil + } + } + // Use default time range if not specified if start == "" { start = fmt.Sprintf("%d", time.Now().Add(-1*time.Hour).Unix()) @@ -101,7 +162,12 @@ func handlePrometheusRangeQueryTool(ctx context.Context, request mcp.CallToolReq fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode()) client := getHTTPClient(ctx) - resp, err := client.Get(fullURL) + req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil) + if err != nil { + return mcp.NewToolResultError("failed to create request: " + err.Error()), nil + } + + resp, err := client.Do(req) if err != nil { return mcp.NewToolResultError("failed to query Prometheus: " + err.Error()), nil } @@ -133,23 +199,48 @@ func handlePrometheusRangeQueryTool(ctx context.Context, request mcp.CallToolReq func handlePrometheusLabelsQueryTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { prometheusURL := mcp.ParseString(request, "prometheus_url", "http://localhost:9090") + // Validate prometheus URL + if err := security.ValidateURL(prometheusURL); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid Prometheus URL: %v", err)), nil + } + // Make request to Prometheus API for labels apiURL := fmt.Sprintf("%s/api/v1/labels", prometheusURL) client := getHTTPClient(ctx) - resp, err := client.Get(apiURL) + req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { - return mcp.NewToolResultError("failed to query Prometheus: " + err.Error()), nil + toolErr := errors.NewPrometheusError("create_request", err). + WithContext("prometheus_url", prometheusURL). + WithContext("api_url", apiURL) + return toolErr.ToMCPResult(), nil + } + + resp, err := client.Do(req) + if err != nil { + toolErr := errors.NewPrometheusError("query_execution", err). + WithContext("prometheus_url", prometheusURL). + WithContext("api_url", apiURL) + return toolErr.ToMCPResult(), nil } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return mcp.NewToolResultError("failed to read response: " + err.Error()), nil + toolErr := errors.NewPrometheusError("read_response", err). + WithContext("prometheus_url", prometheusURL). + WithContext("api_url", apiURL). + WithContext("status_code", resp.StatusCode) + return toolErr.ToMCPResult(), nil } if resp.StatusCode != http.StatusOK { - return mcp.NewToolResultError(fmt.Sprintf("Prometheus API error (%d): %s", resp.StatusCode, string(body))), nil + toolErr := errors.NewPrometheusError("api_error", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))). + WithContext("prometheus_url", prometheusURL). + WithContext("api_url", apiURL). + WithContext("status_code", resp.StatusCode). + WithContext("response_body", string(body)) + return toolErr.ToMCPResult(), nil } // Parse the JSON response to pretty-print it @@ -169,11 +260,21 @@ func handlePrometheusLabelsQueryTool(ctx context.Context, request mcp.CallToolRe func handlePrometheusTargetsQueryTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { prometheusURL := mcp.ParseString(request, "prometheus_url", "http://localhost:9090") + // Validate prometheus URL + if err := security.ValidateURL(prometheusURL); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid Prometheus URL: %v", err)), nil + } + // Make request to Prometheus API for targets apiURL := fmt.Sprintf("%s/api/v1/targets", prometheusURL) client := getHTTPClient(ctx) - resp, err := client.Get(apiURL) + req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) + if err != nil { + return mcp.NewToolResultError("failed to create request: " + err.Error()), nil + } + + resp, err := client.Do(req) if err != nil { return mcp.NewToolResultError("failed to query Prometheus: " + err.Error()), nil } @@ -202,12 +303,12 @@ func handlePrometheusTargetsQueryTool(ctx context.Context, request mcp.CallToolR return mcp.NewToolResultText(string(prettyJSON)), nil } -func RegisterPrometheusTools(s *server.MCPServer, kubeconfig string) { +func RegisterTools(s *server.MCPServer) { s.AddTool(mcp.NewTool("prometheus_query_tool", mcp.WithDescription("Execute a PromQL query against Prometheus"), mcp.WithString("query", mcp.Description("PromQL query to execute"), mcp.Required()), mcp.WithString("prometheus_url", mcp.Description("Prometheus server URL (default: http://localhost:9090)")), - ), handlePrometheusQueryTool) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("prometheus_query_tool", handlePrometheusQueryTool))) s.AddTool(mcp.NewTool("prometheus_query_range_tool", mcp.WithDescription("Execute a PromQL range query against Prometheus"), @@ -216,20 +317,20 @@ func RegisterPrometheusTools(s *server.MCPServer, kubeconfig string) { mcp.WithString("end", mcp.Description("End time (Unix timestamp or relative time)")), mcp.WithString("step", mcp.Description("Query resolution step (default: 15s)")), mcp.WithString("prometheus_url", mcp.Description("Prometheus server URL (default: http://localhost:9090)")), - ), handlePrometheusRangeQueryTool) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("prometheus_query_range_tool", handlePrometheusRangeQueryTool))) s.AddTool(mcp.NewTool("prometheus_label_names_tool", mcp.WithDescription("Get all available labels from Prometheus"), mcp.WithString("prometheus_url", mcp.Description("Prometheus server URL (default: http://localhost:9090)")), - ), handlePrometheusLabelsQueryTool) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("prometheus_label_names_tool", handlePrometheusLabelsQueryTool))) s.AddTool(mcp.NewTool("prometheus_targets_tool", mcp.WithDescription("Get all Prometheus targets and their status"), mcp.WithString("prometheus_url", mcp.Description("Prometheus server URL (default: http://localhost:9090)")), - ), handlePrometheusTargetsQueryTool) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("prometheus_targets_tool", handlePrometheusTargetsQueryTool))) s.AddTool(mcp.NewTool("prometheus_promql_tool", mcp.WithDescription("Generate a PromQL query"), mcp.WithString("query_description", mcp.Description("A string describing the query to generate"), mcp.Required()), - ), handlePromql) + ), telemetry.AdaptToolHandler(telemetry.WithTracing("prometheus_promql_tool", handlePromql))) } diff --git a/pkg/prometheus/prometheus_test.go b/pkg/prometheus/prometheus_test.go index d51b52e5..647d1f39 100644 --- a/pkg/prometheus/prometheus_test.go +++ b/pkg/prometheus/prometheus_test.go @@ -122,7 +122,7 @@ func TestHandlePrometheusQueryTool(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "failed to query Prometheus") + assert.Contains(t, getResultText(result), "**Prometheus Error**") }) t.Run("HTTP 500 error", func(t *testing.T) { @@ -139,7 +139,7 @@ func TestHandlePrometheusQueryTool(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "Prometheus API error (500)") + assert.Contains(t, getResultText(result), "**Prometheus Error**") }) t.Run("malformed JSON response", func(t *testing.T) { @@ -283,7 +283,7 @@ func TestHandlePrometheusLabelsQueryTool(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result) assert.True(t, result.IsError) - assert.Contains(t, getResultText(result), "failed to query Prometheus") + assert.Contains(t, getResultText(result), "**Prometheus Error**") }) t.Run("custom prometheus URL", func(t *testing.T) { diff --git a/pkg/utils/common.go b/pkg/utils/common.go index d8be795d..ce8b73bf 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -3,314 +3,49 @@ package utils import ( "context" "fmt" - "os/exec" - "runtime" "strings" + "sync" "time" - "github.com/kagent-dev/tools/pkg/logger" + "github.com/kagent-dev/tools/internal/commands" + "github.com/kagent-dev/tools/internal/logger" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/metric" ) -// ShellExecutor defines the interface for executing shell commands -type ShellExecutor interface { - Exec(ctx context.Context, command string, args ...string) (output []byte, err error) +// KubeConfigManager manages kubeconfig path with thread safety +type KubeConfigManager struct { + mu sync.RWMutex + kubeconfigPath string } -// DefaultShellExecutor implements ShellExecutor using os/exec -type DefaultShellExecutor struct{} +// globalKubeConfigManager is the singleton instance +var globalKubeConfigManager = &KubeConfigManager{} -// Exec executes a command using os/exec.CommandContext -func (e *DefaultShellExecutor) Exec(ctx context.Context, command string, args ...string) ([]byte, error) { - cmd := exec.CommandContext(ctx, command, args...) - return cmd.CombinedOutput() -} - -// MockShellExecutor implements ShellExecutor for testing -type MockShellExecutor struct { - // Commands maps command+args to expected output and error - Commands map[string]MockCommandResult - // CallLog keeps track of all executed commands for verification - CallLog []MockCommandCall - // PartialMatchers allows partial matching for dynamic arguments - PartialMatchers []PartialMatcher -} - -// PartialMatcher represents a partial command matcher for dynamic arguments -type PartialMatcher struct { - Command string - Args []string // Use "*" for wildcard matching - Result MockCommandResult -} - -// MockCommandResult represents the expected result of a mocked command -type MockCommandResult struct { - Output []byte - Error error -} - -// MockCommandCall represents a logged command execution -type MockCommandCall struct { - Command string - Args []string -} - -// Exec executes a mocked command -func (m *MockShellExecutor) Exec(ctx context.Context, command string, args ...string) ([]byte, error) { - // Log the call - m.CallLog = append(m.CallLog, MockCommandCall{ - Command: command, - Args: args, - }) - - // Try exact match first - key := m.commandKey(command, args...) - if result, exists := m.Commands[key]; exists { - return result.Output, result.Error - } - - // Try partial matchers - for _, matcher := range m.PartialMatchers { - if m.matchesPartial(command, args, matcher) { - return matcher.Result.Output, matcher.Result.Error - } - } - - // Default behavior for unmocked commands - return []byte(""), fmt.Errorf("unmocked command: %s %v", command, args) -} - -// matchesPartial checks if a command matches a partial matcher -func (m *MockShellExecutor) matchesPartial(command string, args []string, matcher PartialMatcher) bool { - if command != matcher.Command { - return false - } - - if len(args) != len(matcher.Args) { - return false - } +// SetKubeconfig sets the global kubeconfig path in a thread-safe manner +func SetKubeconfig(path string) { + globalKubeConfigManager.mu.Lock() + defer globalKubeConfigManager.mu.Unlock() - for i, expectedArg := range matcher.Args { - if expectedArg == "*" { - continue // Wildcard match - } - if args[i] != expectedArg { - return false - } - } - - return true + globalKubeConfigManager.kubeconfigPath = path + logger.Get().Info("Setting shared kubeconfig", "path", path) } -// AddCommand adds a command mock -func (m *MockShellExecutor) AddCommand(command string, args []string, output []byte, err error) { - if m.Commands == nil { - m.Commands = make(map[string]MockCommandResult) - } - key := m.commandKey(command, args...) - m.Commands[key] = MockCommandResult{ - Output: output, - Error: err, - } -} +// GetKubeconfig returns the global kubeconfig path in a thread-safe manner +func GetKubeconfig() string { + globalKubeConfigManager.mu.RLock() + defer globalKubeConfigManager.mu.RUnlock() -// AddCommandString is a convenience method for adding string output -func (m *MockShellExecutor) AddCommandString(command string, args []string, output string, err error) { - m.AddCommand(command, args, []byte(output), err) + return globalKubeConfigManager.kubeconfigPath } -// AddPartialMatcher adds a partial matcher for dynamic arguments -func (m *MockShellExecutor) AddPartialMatcher(command string, args []string, output []byte, err error) { - if m.PartialMatchers == nil { - m.PartialMatchers = []PartialMatcher{} +// AddKubeconfigArgs adds kubeconfig arguments to command args if configured +func AddKubeconfigArgs(args []string) []string { + kubeconfigPath := GetKubeconfig() + if kubeconfigPath != "" { + return append([]string{"--kubeconfig", kubeconfigPath}, args...) } - m.PartialMatchers = append(m.PartialMatchers, PartialMatcher{ - Command: command, - Args: args, - Result: MockCommandResult{ - Output: output, - Error: err, - }, - }) -} - -// AddPartialMatcherString is a convenience method for adding string output with partial matching -func (m *MockShellExecutor) AddPartialMatcherString(command string, args []string, output string, err error) { - m.AddPartialMatcher(command, args, []byte(output), err) -} - -// GetCallLog returns the log of all command calls -func (m *MockShellExecutor) GetCallLog() []MockCommandCall { - return m.CallLog -} - -// Reset clears the mock state -func (m *MockShellExecutor) Reset() { - m.Commands = make(map[string]MockCommandResult) - m.CallLog = []MockCommandCall{} - m.PartialMatchers = []PartialMatcher{} -} - -// commandKey creates a unique key for command+args combination -func (m *MockShellExecutor) commandKey(command string, args ...string) string { - return fmt.Sprintf("%s %s", command, strings.Join(args, " ")) -} - -// Context key for shell executor injection -type contextKey string - -const shellExecutorKey contextKey = "shellExecutor" - -// WithShellExecutor returns a context with the given shell executor -func WithShellExecutor(ctx context.Context, executor ShellExecutor) context.Context { - return context.WithValue(ctx, shellExecutorKey, executor) -} - -// GetShellExecutor retrieves the shell executor from context, or returns default -func GetShellExecutor(ctx context.Context) ShellExecutor { - if executor, ok := ctx.Value(shellExecutorKey).(ShellExecutor); ok { - return executor - } - return &DefaultShellExecutor{} -} - -// NewMockShellExecutor creates a new mock shell executor for testing -func NewMockShellExecutor() *MockShellExecutor { - return &MockShellExecutor{ - Commands: make(map[string]MockCommandResult), - CallLog: []MockCommandCall{}, - PartialMatchers: []PartialMatcher{}, - } -} - -var ( - tracer = otel.Tracer("kagent-tools") - meter = otel.Meter("kagent-tools") - - // Metrics - commandExecutionCounter metric.Int64Counter - commandExecutionDuration metric.Float64Histogram - commandExecutionErrors metric.Int64Counter -) - -func init() { - // Initialize metrics (these are safe to call even if OTEL is not configured) - var err error - - commandExecutionCounter, err = meter.Int64Counter( - "command_executions_total", - metric.WithDescription("Total number of command executions"), - ) - if err != nil { - logger.Get().Error(err, "Failed to create command execution counter") - } - - commandExecutionDuration, err = meter.Float64Histogram( - "command_execution_duration_seconds", - metric.WithDescription("Duration of command executions in seconds"), - metric.WithUnit("s"), - ) - if err != nil { - logger.Get().Error(err, "Failed to create command execution duration histogram") - } - - commandExecutionErrors, err = meter.Int64Counter( - "command_execution_errors_total", - metric.WithDescription("Total number of command execution errors"), - ) - if err != nil { - logger.Get().Error(err, "Failed to create command execution errors counter") - } -} - -// RunCommand executes a command and returns output or error with OTEL tracing -func RunCommand(command string, args []string) (string, error) { - return RunCommandWithContext(context.Background(), command, args) -} - -// RunCommandWithContext executes a command with context and returns output or error with OTEL tracing -func RunCommandWithContext(ctx context.Context, command string, args []string) (string, error) { - // Get caller information for tracing - _, file, line, _ := runtime.Caller(1) - caller := fmt.Sprintf("%s:%d", file, line) - - // Start OpenTelemetry span - spanName := fmt.Sprintf("exec.%s", command) - ctx, span := tracer.Start(ctx, spanName) - defer span.End() - - // Set span attributes - span.SetAttributes( - attribute.String("command", command), - attribute.StringSlice("args", args), - attribute.String("caller", caller), - ) - - // Record metrics - startTime := time.Now() - - // Use the shell executor from context (or default) - executor := GetShellExecutor(ctx) - output, err := executor.Exec(ctx, command, args...) - - duration := time.Since(startTime) - - // Set additional span attributes with results - span.SetAttributes( - attribute.Float64("duration_seconds", duration.Seconds()), - attribute.Int("output_size", len(output)), - ) - - // Record metrics - attributes := []attribute.KeyValue{ - attribute.String("command", command), - attribute.Bool("success", err == nil), - } - - if commandExecutionCounter != nil { - commandExecutionCounter.Add(ctx, 1, metric.WithAttributes(attributes...)) - } - - if commandExecutionDuration != nil { - commandExecutionDuration.Record(ctx, duration.Seconds(), metric.WithAttributes(attributes...)) - } - - if err != nil { - // Set span status and record error - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - span.SetAttributes(attribute.String("error", err.Error())) - - if commandExecutionErrors != nil { - commandExecutionErrors.Add(ctx, 1, metric.WithAttributes(attributes...)) - } - - logger.Get().Error(err, "CommandExec failed", - "command", command, - "args", args, - "duration", duration, - "caller", caller, - ) - return "", fmt.Errorf("command %s failed: %v", command, err) - } - - // Set successful span status - span.SetStatus(codes.Ok, "CommandExec") - - logger.Get().Info("CommandExec", - "command", command, - "args", args, - "duration", duration, - "outputSize", len(output), - "caller", caller, - ) - - return strings.TrimSpace(string(output)), nil + return args } // shellTool provides shell command execution functionality @@ -328,10 +63,21 @@ func shellTool(ctx context.Context, params shellParams) (string, error) { cmd := parts[0] args := parts[1:] - return RunCommandWithContext(ctx, cmd, args) + return commands.NewCommandBuilder(cmd).WithArgs(args...).Execute(ctx) +} + +// handleGetCurrentDateTimeTool provides datetime functionality for both MCP and testing +func handleGetCurrentDateTimeTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Returns the current date and time in ISO 8601 format (RFC3339) + // This matches the Python implementation: datetime.datetime.now().isoformat() + now := time.Now() + return mcp.NewToolResultText(now.Format(time.RFC3339)), nil } -func RegisterCommonTools(s *server.MCPServer) { +func RegisterTools(s *server.MCPServer) { + logger.Get().Info("RegisterTools initialized") + + // Register shell tool s.AddTool(mcp.NewTool("shell", mcp.WithDescription("Execute shell commands"), mcp.WithString("command", mcp.Description("The shell command to execute"), mcp.Required()), @@ -350,5 +96,10 @@ func RegisterCommonTools(s *server.MCPServer) { return mcp.NewToolResultText(result), nil }) + // Register datetime tool + s.AddTool(mcp.NewTool("datetime_get_current_time", + mcp.WithDescription("Returns the current date and time in ISO 8601 format."), + ), handleGetCurrentDateTimeTool) + // Note: LLM Tool implementation would go here if needed } diff --git a/pkg/utils/common_test.go b/pkg/utils/common_test.go deleted file mode 100644 index e21cf764..00000000 --- a/pkg/utils/common_test.go +++ /dev/null @@ -1,288 +0,0 @@ -package utils - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestDefaultShellExecutor(t *testing.T) { - executor := &DefaultShellExecutor{} - - // Test successful command - output, err := executor.Exec(context.Background(), "echo", "hello") - assert.NoError(t, err) - assert.Equal(t, "hello\n", string(output)) - - // Test command with error - output, err = executor.Exec(context.Background(), "nonexistent-command") - assert.Error(t, err) - assert.Empty(t, output) -} - -func TestMockShellExecutor(t *testing.T) { - mock := NewMockShellExecutor() - - t.Run("unmocked command returns error", func(t *testing.T) { - output, err := mock.Exec(context.Background(), "unmocked", "command") - assert.Error(t, err) - assert.Contains(t, err.Error(), "unmocked command") - assert.Empty(t, output) - }) - - t.Run("mocked command returns expected result", func(t *testing.T) { - expectedOutput := "mocked output" - mock.AddCommandString("kubectl", []string{"get", "pods"}, expectedOutput, nil) - - output, err := mock.Exec(context.Background(), "kubectl", "get", "pods") - assert.NoError(t, err) - assert.Equal(t, expectedOutput, string(output)) - }) - - t.Run("mocked command with error", func(t *testing.T) { - expectedError := errors.New("mocked error") - mock.AddCommandString("helm", []string{"install", "app"}, "", expectedError) - - output, err := mock.Exec(context.Background(), "helm", "install", "app") - assert.Error(t, err) - assert.Equal(t, expectedError, err) - assert.Empty(t, output) - }) - - t.Run("call log tracking", func(t *testing.T) { - mock.Reset() - - // Execute some commands - mock.AddCommandString("cmd1", []string{"arg1"}, "output1", nil) - mock.AddCommandString("cmd2", []string{"arg2", "arg3"}, "output2", nil) - - _, _ = mock.Exec(context.Background(), "cmd1", "arg1") - _, _ = mock.Exec(context.Background(), "cmd2", "arg2", "arg3") - _, _ = mock.Exec(context.Background(), "unmocked", "command") - - callLog := mock.GetCallLog() - require.Len(t, callLog, 3) - - assert.Equal(t, "cmd1", callLog[0].Command) - assert.Equal(t, []string{"arg1"}, callLog[0].Args) - - assert.Equal(t, "cmd2", callLog[1].Command) - assert.Equal(t, []string{"arg2", "arg3"}, callLog[1].Args) - - assert.Equal(t, "unmocked", callLog[2].Command) - assert.Equal(t, []string{"command"}, callLog[2].Args) - }) - - t.Run("reset functionality", func(t *testing.T) { - // Create a fresh mock for this test - freshMock := NewMockShellExecutor() - freshMock.AddCommandString("test", []string{}, "output", nil) - _, _ = freshMock.Exec(context.Background(), "test") - - assert.Len(t, freshMock.Commands, 1) - assert.Len(t, freshMock.CallLog, 1) - - freshMock.Reset() - - assert.Len(t, freshMock.Commands, 0) - assert.Len(t, freshMock.CallLog, 0) - }) -} - -func TestContextShellExecutor(t *testing.T) { - t.Run("default executor when no context value", func(t *testing.T) { - ctx := context.Background() - executor := GetShellExecutor(ctx) - - _, ok := executor.(*DefaultShellExecutor) - assert.True(t, ok, "should return DefaultShellExecutor when no context value") - }) - - t.Run("mock executor from context", func(t *testing.T) { - mock := NewMockShellExecutor() - ctx := WithShellExecutor(context.Background(), mock) - - executor := GetShellExecutor(ctx) - assert.Equal(t, mock, executor, "should return the mock executor from context") - }) - - t.Run("context propagation", func(t *testing.T) { - mock := NewMockShellExecutor() - mock.AddCommandString("test", []string{"arg"}, "test output", nil) - - ctx := WithShellExecutor(context.Background(), mock) - - // Test that RunCommandWithContext uses the mock - output, err := RunCommandWithContext(ctx, "test", []string{"arg"}) - assert.NoError(t, err) - assert.Equal(t, "test output", output) - - // Verify the command was logged - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "test", callLog[0].Command) - assert.Equal(t, []string{"arg"}, callLog[0].Args) - }) -} - -func TestRunCommandWithMocking(t *testing.T) { - t.Run("successful command execution with mock", func(t *testing.T) { - mock := NewMockShellExecutor() - mock.AddCommandString("kubectl", []string{"get", "pods", "-n", "default"}, "pod1\npod2", nil) - - ctx := WithShellExecutor(context.Background(), mock) - - output, err := RunCommandWithContext(ctx, "kubectl", []string{"get", "pods", "-n", "default"}) - assert.NoError(t, err) - assert.Equal(t, "pod1\npod2", output) - - // Verify command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"get", "pods", "-n", "default"}, callLog[0].Args) - }) - - t.Run("command failure with mock", func(t *testing.T) { - mock := NewMockShellExecutor() - expectedError := errors.New("command failed") - mock.AddCommandString("helm", []string{"install", "app"}, "", expectedError) - - ctx := WithShellExecutor(context.Background(), mock) - - output, err := RunCommandWithContext(ctx, "helm", []string{"install", "app"}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "command helm failed") - assert.Empty(t, output) - }) - - t.Run("multiple commands with mock", func(t *testing.T) { - mock := NewMockShellExecutor() - mock.AddCommandString("kubectl", []string{"get", "pods"}, "pod-list", nil) - mock.AddCommandString("kubectl", []string{"get", "services"}, "service-list", nil) - mock.AddCommandString("helm", []string{"list"}, "helm-releases", nil) - - ctx := WithShellExecutor(context.Background(), mock) - - // Execute multiple commands - output1, err1 := RunCommandWithContext(ctx, "kubectl", []string{"get", "pods"}) - assert.NoError(t, err1) - assert.Equal(t, "pod-list", output1) - - output2, err2 := RunCommandWithContext(ctx, "kubectl", []string{"get", "services"}) - assert.NoError(t, err2) - assert.Equal(t, "service-list", output2) - - output3, err3 := RunCommandWithContext(ctx, "helm", []string{"list"}) - assert.NoError(t, err3) - assert.Equal(t, "helm-releases", output3) - - // Verify all commands were logged - callLog := mock.GetCallLog() - require.Len(t, callLog, 3) - - assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"get", "pods"}, callLog[0].Args) - - assert.Equal(t, "kubectl", callLog[1].Command) - assert.Equal(t, []string{"get", "services"}, callLog[1].Args) - - assert.Equal(t, "helm", callLog[2].Command) - assert.Equal(t, []string{"list"}, callLog[2].Args) - }) -} - -func TestShellToolWithMocking(t *testing.T) { - t.Run("shell tool uses mock executor", func(t *testing.T) { - mock := NewMockShellExecutor() - mock.AddCommandString("echo", []string{"hello", "world"}, "hello world", nil) - - ctx := WithShellExecutor(context.Background(), mock) - - params := shellParams{Command: "echo hello world"} - output, err := shellTool(ctx, params) - assert.NoError(t, err) - assert.Equal(t, "hello world", output) - - // Verify command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "echo", callLog[0].Command) - assert.Equal(t, []string{"hello", "world"}, callLog[0].Args) - }) - - t.Run("shell tool with empty command", func(t *testing.T) { - mock := NewMockShellExecutor() - ctx := WithShellExecutor(context.Background(), mock) - - params := shellParams{Command: ""} - output, err := shellTool(ctx, params) - assert.Error(t, err) - assert.Contains(t, err.Error(), "empty command") - assert.Empty(t, output) - - // No commands should be logged - callLog := mock.GetCallLog() - assert.Len(t, callLog, 0) - }) -} - -func TestMockShellExecutorCommandKey(t *testing.T) { - mock := NewMockShellExecutor() - - // Test that different argument combinations create different keys - mock.AddCommandString("kubectl", []string{"get", "pods"}, "pods", nil) - mock.AddCommandString("kubectl", []string{"get", "services"}, "services", nil) - mock.AddCommandString("kubectl", []string{}, "kubectl-help", nil) - - // Test first command - output, err := mock.Exec(context.Background(), "kubectl", "get", "pods") - assert.NoError(t, err) - assert.Equal(t, "pods", string(output)) - - // Test second command - output, err = mock.Exec(context.Background(), "kubectl", "get", "services") - assert.NoError(t, err) - assert.Equal(t, "services", string(output)) - - // Test third command (no args) - output, err = mock.Exec(context.Background(), "kubectl") - assert.NoError(t, err) - assert.Equal(t, "kubectl-help", string(output)) -} - -// Benchmark tests to ensure mocking doesn't add significant overhead -func BenchmarkDefaultShellExecutor(b *testing.B) { - executor := &DefaultShellExecutor{} - ctx := context.Background() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _ = executor.Exec(ctx, "echo", "test") - } -} - -func BenchmarkMockShellExecutor(b *testing.B) { - mock := NewMockShellExecutor() - mock.AddCommandString("echo", []string{"test"}, "test", nil) - ctx := context.Background() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _ = mock.Exec(ctx, "echo", "test") - } -} - -func BenchmarkRunCommandWithContext(b *testing.B) { - mock := NewMockShellExecutor() - mock.AddCommandString("echo", []string{"test"}, "test", nil) - ctx := WithShellExecutor(context.Background(), mock) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _ = RunCommandWithContext(ctx, "echo", []string{"test"}) - } -} diff --git a/pkg/utils/datetime.go b/pkg/utils/datetime.go index 165ea683..3bca950b 100644 --- a/pkg/utils/datetime.go +++ b/pkg/utils/datetime.go @@ -1,31 +1,5 @@ package utils -import ( - "context" - "github.com/kagent-dev/tools/pkg/logger" - "time" - - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" -) - -var kubeConfig = "" - -// DateTime tools using direct Go time package -// This implementation matches the Python version exactly -func handleGetCurrentDateTimeTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - // Returns the current date and time in ISO 8601 format (RFC3339) - // This matches the Python implementation: datetime.datetime.now().isoformat() - now := time.Now() - return mcp.NewToolResultText(now.Format(time.RFC3339)), nil -} - -func RegisterDateTimeTools(s *server.MCPServer, kubeconfig string) { - kubeConfig = kubeconfig - logger.Get().Info("kubeConfig", kubeConfig) - - // Register the GetCurrentDateTime tool to match Python implementation exactly - s.AddTool(mcp.NewTool("datetime_get_current_time", - mcp.WithDescription("Returns the current date and time in ISO 8601 format."), - ), handleGetCurrentDateTimeTool) -} +// DateTime tools implementation moved to RegisterTools function in common.go +// This file remains for backwards compatibility but the tools are now registered +// through the unified RegisterTools function. From 0daa9ee17bdf2c452ec5b60e8230c372d94317bb Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Tue, 22 Jul 2025 14:51:12 +0200 Subject: [PATCH 18/41] Tools chart & e2e with k8s (#12) --- .github/workflows/ci.yaml | 6 + .github/workflows/tag.yaml | 1 + .gitignore | 2 + Makefile | 74 +- README.md | 15 + cmd/main.go | 30 +- e2e/e2e_test.go | 1005 - go.mod | 52 +- go.sum | 148 + helm/kagent-tools/Chart-template.yaml | 5 + helm/kagent-tools/templates/NOTES.txt | 10 + helm/kagent-tools/templates/_helpers.tpl | 75 + helm/kagent-tools/templates/clusterrole.yaml | 29 + .../templates/clusterrolebinding.yaml | 44 + helm/kagent-tools/templates/deployment.yaml | 74 + helm/kagent-tools/templates/service.yaml | 21 + .../templates/serviceaccount.yaml | 7 + helm/kagent-tools/values.yaml | 61 + internal/commands/builder.go | 78 +- internal/commands/builder_test.go | 8 +- internal/security/validation.go | 4 - pkg/argo/argo.go | 27 + pkg/argo/argo_test.go | 57 +- pkg/cilium/cilium_test.go | 30 +- pkg/helm/helm.go | 15 +- pkg/helm/helm_test.go | 222 +- pkg/istio/istio_test.go | 34 +- pkg/k8s/k8s.go | 18 +- pkg/k8s/k8s_test.go | 16 +- reports/cve-report.tmpl | 4 + scripts/install.sh | 252 + scripts/kind/crd-argo.yaml | 16470 ++++++++++++++++ scripts/kind/kind-config.yaml | 39 + scripts/kind/test-values-e2e.yaml | 18 + scripts/kind/test-values.yaml | 18 + test/e2e/cli_test.go | 633 + test/e2e/e2e_test.go | 13 + test/e2e/helpers_test.go | 606 + test/e2e/k8s_test.go | 151 + 39 files changed, 19092 insertions(+), 1280 deletions(-) delete mode 100644 e2e/e2e_test.go create mode 100644 helm/kagent-tools/Chart-template.yaml create mode 100644 helm/kagent-tools/templates/NOTES.txt create mode 100644 helm/kagent-tools/templates/_helpers.tpl create mode 100644 helm/kagent-tools/templates/clusterrole.yaml create mode 100644 helm/kagent-tools/templates/clusterrolebinding.yaml create mode 100644 helm/kagent-tools/templates/deployment.yaml create mode 100644 helm/kagent-tools/templates/service.yaml create mode 100644 helm/kagent-tools/templates/serviceaccount.yaml create mode 100644 helm/kagent-tools/values.yaml create mode 100644 reports/cve-report.tmpl create mode 100755 scripts/install.sh create mode 100644 scripts/kind/crd-argo.yaml create mode 100644 scripts/kind/kind-config.yaml create mode 100644 scripts/kind/test-values-e2e.yaml create mode 100644 scripts/kind/test-values.yaml create mode 100644 test/e2e/cli_test.go create mode 100644 test/e2e/e2e_test.go create mode 100644 test/e2e/helpers_test.go create mode 100644 test/e2e/k8s_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4d1c4d63..72a6e43b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -67,6 +67,12 @@ jobs: go-version: "1.24" cache: true + - name: Create k8s Kind Cluster + uses: helm/kind-action@v1 + with: + cluster_name: kagent + config: scripts/kind/kind-config.yaml + - name: Run cmd/main.go tests working-directory: . run: | diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index 025642bc..a1fceee3 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -50,6 +50,7 @@ jobs: export VERSION=$(echo "$GITHUB_REF" | cut -c12-) fi make docker-build + make helm-publish release: # Only run release after images and helm chart are pushed # In the future we can take the chart from the helm action, diff --git a/.gitignore b/.gitignore index 2496f99b..6bd4b641 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ bin/ /kagent-tools /*.out *.html +/helm/kagent-tools/Chart.yaml +/reports/tools-cve.csv diff --git a/Makefile b/Makefile index 1d9fdd11..dea88a80 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,14 @@ DOCKER_REGISTRY ?= ghcr.io BASE_IMAGE_REGISTRY ?= cgr.dev + DOCKER_REPO ?= kagent-dev/kagent + +HELM_REPO ?= oci://ghcr.io/kagent-dev +HELM_ACTION=upgrade --install + KIND_CLUSTER_NAME ?= kagent +KIND_IMAGE_VERSION ?= 1.33.1 +KIND_CREATE_CMD ?= "kind create cluster --name $(KIND_CLUSTER_NAME) --image kindest/node:v$(KIND_IMAGE_VERSION) --config ./scripts/kind/kind-config.yaml" BUILD_DATE := $(shell date -u '+%Y-%m-%d') GIT_COMMIT := $(shell git rev-parse --short HEAD || echo "unknown") @@ -12,6 +19,7 @@ LDFLAGS := -X github.com/kagent-dev/tools/internal/version.Version=$(VERSION) -X ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin +HELM_DIST_FOLDER ?= $(shell pwd)/dist .PHONY: clean clean: @@ -55,8 +63,8 @@ test-only: ## Run tests only (without build/lint for faster iteration) go test -tags=test -v -cover ./pkg/... ./internal/... .PHONY: e2e -e2e: test docker-build - go test -tags=test -v -cover ./e2e/... +e2e: test retag + go test -v -tags=test -cover ./test/e2e/ -timeout 5m bin/kagent-tools-linux-amd64: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/kagent-tools-linux-amd64 ./cmd @@ -89,7 +97,7 @@ bin/kagent-tools-windows-amd64.exe.sha256: bin/kagent-tools-windows-amd64.exe sha256sum bin/kagent-tools-windows-amd64.exe > bin/kagent-tools-windows-amd64.exe.sha256 .PHONY: build -build: $(LOCALBIN) bin/kagent-tools-linux-amd64.sha256 bin/kagent-tools-linux-arm64.sha256 bin/kagent-tools-darwin-amd64.sha256 bin/kagent-tools-darwin-arm64.sha256 bin/kagent-tools-windows-amd64.exe.sha256 +build: $(LOCALBIN) clean bin/kagent-tools-linux-amd64.sha256 bin/kagent-tools-linux-arm64.sha256 bin/kagent-tools-darwin-amd64.sha256 bin/kagent-tools-darwin-arm64.sha256 bin/kagent-tools-windows-amd64.exe.sha256 build: @echo "Build complete. Binaries are available in the bin/ directory." ls -lt bin/kagent-tools-* @@ -100,8 +108,10 @@ run: docker-build @echo "Use: npx @modelcontextprotocol/inspector to connect to the tool server" @docker run --rm --net=host -p 8084:8084 -e OPENAI_API_KEY=$(OPENAI_API_KEY) -v $(HOME)/.kube:/home/nonroot/.kube -e KAGENT_TOOLS_PORT=8084 $(TOOLS_IMG) -- --kubeconfig /root/.kube/config -PHONY: retag -retag: docker-build +.PHONY: retag +retag: docker-build helm-version + @echo "Check Kind cluster $(KIND_CLUSTER_NAME) exists" + kind get clusters | grep -q $(KIND_CLUSTER_NAME) || bash -c $(KIND_CREATE_CMD) @echo "Retagging tools image to $(RETAGGED_TOOLS_IMG)" docker tag $(TOOLS_IMG) $(RETAGGED_TOOLS_IMG) kind load docker-image --name $(KIND_CLUSTER_NAME) $(RETAGGED_TOOLS_IMG) @@ -127,7 +137,7 @@ DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUI TOOLS_ISTIO_VERSION ?= 1.26.2 TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 TOOLS_KUBECTL_VERSION ?= 1.33.2 -TOOLS_HELM_VERSION ?= 3.18.3 +TOOLS_HELM_VERSION ?= 3.18.4 TOOLS_CILIUM_VERSION ?= 0.18.5 # build args @@ -155,11 +165,55 @@ docker-build-all: DOCKER_BUILD_ARGS = --progress=plain --builder $(BUILDX_BUILDE docker-build-all: $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -f Dockerfile ./ +.PHONY: helm-version +helm-version: + VERSION=$(VERSION) envsubst < helm/kagent-tools/Chart-template.yaml > helm/kagent-tools/Chart.yaml + mkdir -p $(HELM_DIST_FOLDER) + helm package -d $(HELM_DIST_FOLDER) helm/kagent-tools + +.PHONY: helm-uninstall +helm-uninstall: + helm uninstall kagent --namespace kagent --kube-context kind-$(KIND_CLUSTER_NAME) --wait + +.PHONY: helm-install +helm-install: helm-version + helm $(HELM_ACTION) kagent-tools ./helm/kagent-tools \ + --kube-context kind-$(KIND_CLUSTER_NAME) \ + --namespace kagent \ + --create-namespace \ + --history-max 2 \ + --timeout 5m \ + -f ./scripts/kind/test-values.yaml \ + --set tools.image.registry=$(RETAGGED_DOCKER_REGISTRY) \ + --wait + +.PHONY: helm-publish +helm-publish: helm-version + helm push ./$(HELM_DIST_FOLDER)/kagent-tools-$(VERSION).tgz $(HELM_REPO)/tools/helm + +.PHONY: create-kind-cluster +create-kind-cluster: + docker pull kindest/node:v$(KIND_IMAGE_VERSION) || true + bash -c $(KIND_CREATE_CMD) + +.PHONY: delete-kind-cluster +delete-kind-cluster: + kind delete cluster --name $(KIND_CLUSTER_NAME) + .PHONY: kind-update-kagent -kind-update-kagent: docker-build - kind get clusters | grep -q $(KIND_CLUSTER_NAME) || kind create cluster --name $(KIND_CLUSTER_NAME) - kind load docker-image --name $(KIND_CLUSTER_NAME) $(TOOLS_IMG) - kubectl patch --namespace kagent deployment/kagent --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/3/image", "value": "$(TOOLS_IMG)"}]' +kind-update-kagent: retag + kubectl patch --namespace kagent deployment/kagent --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/3/image", "value": "$(RETAGGED_TOOLS_IMG)"}]' + +.PHONY: otel-local +otel-local: + docker rm -f jaeger-desktop || true + docker run -d --name jaeger-desktop --restart=always -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/jaeger:2.7.0 + open http://localhost:16686/ + +.PHONY: report/image-cve +report/image-cve: docker-build govulncheck + echo "Running CVE scan :: CVE -> CSV ... reports/$(SEMVER)/" + grype docker:$(TOOLS_IMG) -o template -t reports/cve-report.tmpl --file reports/$(SEMVER)/tools-cve.csv ## Tool Binaries ## Location to install dependencies t diff --git a/README.md b/README.md index 56ab7c62..f26ccf9d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,21 @@ This directory contains the Go implementation of all KAgent tools, migrated from the original Python implementation. The tools are designed to work with the Model Context Protocol (MCP) server and provide comprehensive Kubernetes, cloud-native, and observability functionality. +## Installation + +- **Bash:** + +`curl -sL https://github.com/kagent-dev/tools/blob/main/scripts/install.sh | bash` + +- **Docker:** + +`docker run -it --rm ghcr.io/kagent-dev/kagent/tools:` + +- **Kubernetes** + +`helm upgrade -i kagent-tools --version oci://ghcr.io/kagent-dev/tools/helm/` + + ## Architecture The Go tools are implemented as a single MCP server that exposes all available tools through the MCP protocol. diff --git a/cmd/main.go b/cmd/main.go index 6ea40ae7..03a26658 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -33,10 +33,11 @@ import ( ) var ( - port int - stdio bool - tools []string - kubeconfig *string + port int + stdio bool + tools []string + kubeconfig *string + showVersion bool // These variables should be set during build time using -ldflags Name = "kagent-tools-server" @@ -55,6 +56,7 @@ func init() { rootCmd.Flags().IntVarP(&port, "port", "p", 8084, "Port to run the server on") rootCmd.Flags().BoolVar(&stdio, "stdio", false, "Use stdio for communication instead of HTTP") rootCmd.Flags().StringSliceVar(&tools, "tools", []string{}, "List of tools to register. If empty, all tools are registered.") + rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information and exit") kubeconfig = rootCmd.Flags().String("kubeconfig", "", "kubeconfig file path (optional, defaults to in-cluster config)") // if found .env file, load it @@ -70,7 +72,23 @@ func main() { } } +// printVersion displays version information in a formatted way +func printVersion() { + fmt.Printf("%s\n", Name) + fmt.Printf("Version: %s\n", Version) + fmt.Printf("Git Commit: %s\n", GitCommit) + fmt.Printf("Build Date: %s\n", BuildDate) + fmt.Printf("Go Version: %s\n", runtime.Version()) + fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH) +} + func run(cmd *cobra.Command, args []string) { + // Handle version flag early, before any initialization + if showVersion { + printVersion() + return + } + logger.Init() defer logger.Sync() @@ -130,7 +148,9 @@ func run(cmd *cobra.Command, args []string) { runStdioServer(ctx, mcp) }() } else { - sseServer := server.NewStreamableHTTPServer(mcp) + sseServer := server.NewStreamableHTTPServer(mcp, + server.WithHeartbeatInterval(30*time.Second), + ) // Create a mux to handle different routes mux := http.NewServeMux() diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go deleted file mode 100644 index ec047514..00000000 --- a/e2e/e2e_test.go +++ /dev/null @@ -1,1005 +0,0 @@ -package e2e - -import ( - "bufio" - "context" - "fmt" - "io" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// getBinaryName returns the platform-specific binary name -func getBinaryName() string { - osName := runtime.GOOS - archName := runtime.GOARCH - return fmt.Sprintf("kagent-tools-%s-%s", osName, archName) -} - -// TestServerConfig holds configuration for server tests -type TestServerConfig struct { - Port int - Tools []string - Kubeconfig string - Stdio bool - Timeout time.Duration -} - -// ServerTestResult holds the result of a server test -type ServerTestResult struct { - Output string - Error error - Duration time.Duration -} - -// TestServer represents a test server instance -type TestServer struct { - cmd *exec.Cmd - port int - stdio bool - cancel context.CancelFunc - done chan struct{} - output strings.Builder - mu sync.RWMutex -} - -// NewTestServer creates a new test server instance -func NewTestServer(config TestServerConfig) *TestServer { - return &TestServer{ - port: config.Port, - stdio: config.Stdio, - done: make(chan struct{}), - } -} - -// Start starts the test server -func (ts *TestServer) Start(ctx context.Context, config TestServerConfig) error { - ts.mu.Lock() - defer ts.mu.Unlock() - - // Build command arguments - args := []string{} - if config.Stdio { - args = append(args, "--stdio") - } else { - args = append(args, "--port", fmt.Sprintf("%d", config.Port)) - } - - if len(config.Tools) > 0 { - args = append(args, "--tools", strings.Join(config.Tools, ",")) - } - - if config.Kubeconfig != "" { - args = append(args, "--kubeconfig", config.Kubeconfig) - } - - // Create context with cancellation - ctx, cancel := context.WithCancel(ctx) - ts.cancel = cancel - - // Start server process - binaryName := getBinaryName() - ts.cmd = exec.CommandContext(ctx, fmt.Sprintf("../bin/%s", binaryName), args...) - ts.cmd.Env = append(os.Environ(), "LOG_LEVEL=debug") - - // Set up output capture - stdout, err := ts.cmd.StdoutPipe() - if err != nil { - return fmt.Errorf("failed to create stdout pipe: %w", err) - } - - stderr, err := ts.cmd.StderrPipe() - if err != nil { - return fmt.Errorf("failed to create stderr pipe: %w", err) - } - - // Start the command - if err := ts.cmd.Start(); err != nil { - return fmt.Errorf("failed to start server: %w", err) - } - - // Start goroutines to capture output - go ts.captureOutput(stdout, "STDOUT") - go ts.captureOutput(stderr, "STDERR") - - // Wait for server to start - if !config.Stdio { - return ts.waitForHTTPServer(ctx, config.Timeout) - } - - return nil -} - -// Stop stops the test server -func (ts *TestServer) Stop() error { - ts.mu.Lock() - defer ts.mu.Unlock() - - if ts.cancel != nil { - ts.cancel() - } - - if ts.cmd != nil && ts.cmd.Process != nil { - // Send interrupt signal for graceful shutdown - if err := ts.cmd.Process.Signal(os.Interrupt); err != nil { - // If interrupt fails, kill the process - _ = ts.cmd.Process.Kill() - } - - // Wait for process to exit with timeout - done := make(chan error, 1) - go func() { - done <- ts.cmd.Wait() - }() - - select { - case <-done: - // Process exited - case <-time.After(5 * time.Second): - // Timeout, force kill - _ = ts.cmd.Process.Kill() - } - } - - close(ts.done) - return nil -} - -// GetOutput returns the captured output -func (ts *TestServer) GetOutput() string { - ts.mu.RLock() - defer ts.mu.RUnlock() - return ts.output.String() -} - -// captureOutput captures output from the server -func (ts *TestServer) captureOutput(reader io.Reader, prefix string) { - scanner := bufio.NewScanner(reader) - for scanner.Scan() { - line := scanner.Text() - ts.mu.Lock() - ts.output.WriteString(fmt.Sprintf("[%s] %s\n", prefix, line)) - ts.mu.Unlock() - } -} - -// waitForHTTPServer waits for the HTTP server to become available -func (ts *TestServer) waitForHTTPServer(ctx context.Context, timeout time.Duration) error { - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - url := fmt.Sprintf("http://localhost:%d/health", ts.port) - ticker := time.NewTicker(100 * time.Millisecond) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return fmt.Errorf("timeout waiting for server to start") - case <-ticker.C: - resp, err := http.Get(url) - if err == nil { - resp.Body.Close() - if resp.StatusCode == http.StatusOK { - return nil - } - } - } - } -} - -// TestHTTPServerStartup tests basic HTTP server startup and shutdown -func TestHTTPServerStartup(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8085, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - - // Start server - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait a bit for server to be fully ready - time.Sleep(3 * time.Second) - - // Test health endpoint - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) - require.NoError(t, err, "Health endpoint should be accessible") - assert.Equal(t, http.StatusOK, resp.StatusCode) - resp.Body.Close() - - // Check server output - output := server.GetOutput() - assert.Contains(t, output, "Running KAgent Tools Server") - assert.Contains(t, output, fmt.Sprintf(":%d", config.Port)) - - // Stop server - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") - - // Verify server is stopped - time.Sleep(1 * time.Second) - _, err = http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) - assert.Error(t, err, "Server should not be accessible after stop") -} - -// TestHTTPServerWithSpecificTools tests server with specific tools enabled -func TestHTTPServerWithSpecificTools(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8086, - Tools: []string{"utils", "k8s"}, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - - // Start server - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Check server output for tool registration - output := server.GetOutput() - assert.Contains(t, output, "RegisterTools initialized", "Should register specified tools") - assert.Contains(t, output, "utils", "Should register utils tools") - assert.Contains(t, output, "k8s", "Should register k8s tools") - - // Stop server - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestHTTPServerWithAllTools tests server with all tools enabled (default) -func TestHTTPServerWithAllTools(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8087, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - - // Start server - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Check server output for all tools registration - output := server.GetOutput() - assert.Contains(t, output, "RegisterTools initialized", "Should initialize RegisterTools") - - // Verify server is running (tools are implicitly registered when no specific tools are provided) - assert.Contains(t, output, "Running KAgent Tools Server", "Should be running with all tools") - - // Stop server - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestHTTPServerWithKubeconfig tests server with kubeconfig parameter -func TestHTTPServerWithKubeconfig(t *testing.T) { - ctx := context.Background() - - // Create a temporary kubeconfig file - tempDir := t.TempDir() - kubeconfigPath := filepath.Join(tempDir, "kubeconfig") - - kubeconfigContent := `apiVersion: v1 -kind: Config -clusters: -- cluster: - server: https://test-cluster - name: test-cluster -contexts: -- context: - cluster: test-cluster - user: test-user - name: test-context -current-context: test-context -users: -- name: test-user - user: - token: test-token -` - - err := os.WriteFile(kubeconfigPath, []byte(kubeconfigContent), 0644) - require.NoError(t, err, "Should create temporary kubeconfig file") - - config := TestServerConfig{ - Port: 8088, - Kubeconfig: kubeconfigPath, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - - // Start server - err = server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Check server output for kubeconfig setting - output := server.GetOutput() - assert.Contains(t, output, "RegisterTools initialized", "Should initialize RegisterTools") - assert.Contains(t, output, "Running KAgent Tools Server", "Should be running with kubeconfig") - - // Stop server - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestStdioServer tests STDIO server mode -func TestStdioServer(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Stdio: true, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - - // Start server - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Check server output for STDIO mode - output := server.GetOutput() - assert.Contains(t, output, "Running KAgent Tools Server STDIO") - - // Stop server - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestServerGracefulShutdown tests graceful shutdown behavior -func TestServerGracefulShutdown(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8100, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - - // Start server - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Stop server and measure shutdown time - start := time.Now() - err = server.Stop() - duration := time.Since(start) - - require.NoError(t, err, "Server should stop gracefully") - assert.Less(t, duration, 10*time.Second, "Shutdown should complete within reasonable time") - - // Wait a bit for shutdown logs to be captured - time.Sleep(3 * time.Second) - - // Check server output for graceful shutdown - output := server.GetOutput() - // The main test is that the server started successfully and stopped without error - assert.Contains(t, output, "Running KAgent Tools Server", "Server should have started successfully") - - // Try to verify the server is actually stopped by attempting to connect - _, err = http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) - assert.Error(t, err, "Server should not be accessible after stop") -} - -// TestServerWithInvalidTool tests server behavior with invalid tool names -func TestServerWithInvalidTool(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8090, - Tools: []string{"invalid-tool", "utils"}, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - - // Start server - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start even with invalid tools") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Check server output for error about invalid tool - output := server.GetOutput() - assert.Contains(t, output, "Unknown tool specified") - assert.Contains(t, output, "invalid-tool") - - // Valid tools should still be registered - assert.Contains(t, output, "RegisterTools initialized") - assert.Contains(t, output, "utils") - - // Stop server - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestServerVersionAndBuildInfo tests server version and build information -func TestServerVersionAndBuildInfo(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8091, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - - // Start server - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Check server output for version information - output := server.GetOutput() - assert.Contains(t, output, "Starting kagent-tools-server") - assert.Contains(t, output, "version") - - // Stop server - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestConcurrentServerInstances tests running multiple server instances -func TestConcurrentServerInstances(t *testing.T) { - ctx := context.Background() - - var wg sync.WaitGroup - numServers := 3 - servers := make([]*TestServer, numServers) - - // Start multiple servers on different ports - for i := 0; i < numServers; i++ { - wg.Add(1) - go func(index int) { - defer wg.Done() - - config := TestServerConfig{ - Port: 8092 + index, - Tools: []string{"utils"}, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - servers[index] = server - - err := server.Start(ctx, config) - assert.NoError(t, err, fmt.Sprintf("Server %d should start successfully", index)) - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Test health endpoint - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) - assert.NoError(t, err, fmt.Sprintf("Health endpoint should be accessible for server %d", index)) - if resp != nil { - resp.Body.Close() - } - }(i) - } - - wg.Wait() - - // Stop all servers - for i, server := range servers { - if server != nil { - err := server.Stop() - assert.NoError(t, err, fmt.Sprintf("Server %d should stop gracefully", i)) - } - } -} - -// TestServerEnvironmentVariables tests server with environment variables -func TestServerEnvironmentVariables(t *testing.T) { - ctx := context.Background() - - // Set environment variables - originalEnv := os.Environ() - defer func() { - os.Clearenv() - for _, env := range originalEnv { - parts := strings.SplitN(env, "=", 2) - if len(parts) == 2 { - os.Setenv(parts[0], parts[1]) - } - } - }() - - os.Setenv("LOG_LEVEL", "info") - os.Setenv("OTEL_SERVICE_NAME", "test-kagent-tools") - - config := TestServerConfig{ - Port: 8095, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - - // Start server - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Check server output - output := server.GetOutput() - assert.Contains(t, output, "Starting kagent-tools-server") - - // Stop server - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestServerBuildAndExecution tests that the server binary exists and is executable -func TestServerBuildAndExecution(t *testing.T) { - // Check if server binary exists - binaryName := getBinaryName() - binaryPath := fmt.Sprintf("../bin/%s", binaryName) - _, err := os.Stat(binaryPath) - if os.IsNotExist(err) { - t.Skip("Server binary not found, skipping test. Run 'make build' first.") - } - require.NoError(t, err, "Server binary should exist") - - // Test --help flag - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - cmd := exec.CommandContext(ctx, binaryPath, "--help") - output, err := cmd.CombinedOutput() - require.NoError(t, err, "Server should respond to --help flag") - - outputStr := string(output) - assert.Contains(t, outputStr, "KAgent tool server") - assert.Contains(t, outputStr, "--port") - assert.Contains(t, outputStr, "--stdio") - assert.Contains(t, outputStr, "--tools") - assert.Contains(t, outputStr, "--kubeconfig") -} - -// Benchmark tests -func BenchmarkServerStartup(b *testing.B) { - ctx := context.Background() - - for i := 0; i < b.N; i++ { - config := TestServerConfig{ - Port: 8096 + i, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - - start := time.Now() - err := server.Start(ctx, config) - if err != nil { - b.Fatalf("Server startup failed: %v", err) - } - - // Wait for server to be ready - time.Sleep(1 * time.Second) - - duration := time.Since(start) - b.ReportMetric(float64(duration.Nanoseconds()), "startup_time_ns") - - // Stop server - _ = server.Stop() - } -} - -// Helper functions for test setup -func init() { - // Ensure the binary exists before running tests - binaryName := getBinaryName() - binaryPath := fmt.Sprintf("../bin/%s", binaryName) - if _, err := os.Stat(binaryPath); os.IsNotExist(err) { - // Try to build the binary - cmd := exec.Command("make", "build") - cmd.Dir = ".." - if err := cmd.Run(); err != nil { - panic(fmt.Sprintf("Failed to build server binary: %v", err)) - } - } -} - -// TestToolRegistrationValidation tests that tool registration works correctly -func TestToolRegistrationValidation(t *testing.T) { - ctx := context.Background() - - testCases := []struct { - name string - config TestServerConfig - expectedTools []string - shouldFail bool - }{ - { - name: "Register single tool", - config: TestServerConfig{ - Port: 8087, - Tools: []string{"k8s"}, - Timeout: 30 * time.Second, - }, - expectedTools: []string{"k8s"}, - shouldFail: false, - }, - { - name: "Register multiple tools", - config: TestServerConfig{ - Port: 8088, - Tools: []string{"k8s", "prometheus", "utils"}, - Timeout: 30 * time.Second, - }, - expectedTools: []string{"k8s", "prometheus", "utils"}, - shouldFail: false, - }, - { - name: "Register invalid tool", - config: TestServerConfig{ - Port: 8089, - Tools: []string{"invalid-tool"}, - Timeout: 30 * time.Second, - }, - shouldFail: false, - }, - { - name: "Register all tools implicitly", - config: TestServerConfig{ - Port: 8090, - Tools: []string{}, - Timeout: 30 * time.Second, - }, - expectedTools: []string{"utils", "k8s", "prometheus", "helm", "istio", "argo", "cilium"}, - shouldFail: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - server := NewTestServer(tc.config) - err := server.Start(ctx, tc.config) - - if tc.shouldFail { - require.Error(t, err, "Server should fail to start with invalid configuration") - return - } - - require.NoError(t, err, "Server should start successfully") - defer func() { - if err := server.Stop(); err != nil { - t.Errorf("Failed to stop server: %v", err) - } - }() - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Verify registered tools - output := server.GetOutput() - - // Special handling for invalid tool test case - if tc.name == "Register invalid tool" { - assert.Contains(t, output, "Unknown tool specified", "Should warn about invalid tool") - assert.Contains(t, output, "invalid-tool", "Should mention the invalid tool name") - } else { - if tc.name == "Register all tools implicitly" { - // For implicit all tools registration, check for RegisterTools initialized - assert.Contains(t, output, "RegisterTools initialized", "Should initialize RegisterTools") - // Don't check for individual tool names as they're not logged individually - assert.Contains(t, output, "Running KAgent Tools Server", "Should be running with all tools") - } else { - // For specific tools, check for Running server message and tool names - assert.Contains(t, output, "Running KAgent Tools Server", "Should be running server") - for _, tool := range tc.expectedTools { - assert.Contains(t, output, tool, fmt.Sprintf("Should register %s tool", tool)) - } - } - } - - // Test health endpoint - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", tc.config.Port)) - require.NoError(t, err, "Health endpoint should be accessible") - assert.Equal(t, http.StatusOK, resp.StatusCode) - resp.Body.Close() - }) - } -} - -// TestToolExecutionFlow tests the complete flow of tool execution -func TestToolExecutionFlow(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8091, - Tools: []string{"utils"}, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - defer func() { - if err := server.Stop(); err != nil { - t.Errorf("Failed to stop server: %v", err) - } - }() - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Test health endpoint (MCP server doesn't have REST endpoints for tool execution) - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) - require.NoError(t, err, "Should execute request successfully") - defer resp.Body.Close() - - // Check response - assert.Equal(t, http.StatusOK, resp.StatusCode, "Should return OK status") - - // Read response body - body, err := io.ReadAll(resp.Body) - require.NoError(t, err, "Should read response body") - - // Response should contain "OK" - assert.Equal(t, "OK", string(body), "Should return OK response") -} - -// TestServerTelemetry tests that telemetry is properly initialized and working -func TestServerTelemetry(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8092, - Tools: []string{"utils"}, - Timeout: 30 * time.Second, - } - - // Set test environment variables for telemetry - os.Setenv("OTEL_SERVICE_NAME", "kagent-tools-test") - os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317") - defer os.Unsetenv("OTEL_SERVICE_NAME") - defer os.Unsetenv("OTEL_EXPORTER_OTLP_ENDPOINT") - - server := NewTestServer(config) - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - defer func() { - if err := server.Stop(); err != nil { - t.Errorf("Failed to stop server: %v", err) - } - }() - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Check server output for telemetry initialization - output := server.GetOutput() - assert.Contains(t, output, "Starting kagent-tools-server", "Server should start with telemetry") - - // Make a request to generate telemetry - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) - require.NoError(t, err, "Health endpoint should be accessible") - assert.Equal(t, http.StatusOK, resp.StatusCode) - resp.Body.Close() - - // Check server output for successful startup (telemetry is initialized internally) - output = server.GetOutput() - assert.Contains(t, output, "Running KAgent Tools Server", "Server should be running with telemetry enabled") -} - -// TestToolRegistrationWithInvalidNames tests server behavior with invalid tool names -func TestToolRegistrationWithInvalidNames(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8087, - Tools: []string{"invalid-tool", "not-exists", "k8s"}, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully despite invalid tools") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Check server output for warning messages about invalid tools - output := server.GetOutput() - assert.Contains(t, output, "Unknown tool specified") - assert.Contains(t, output, "invalid-tool") - assert.Contains(t, output, "not-exists") - - // Verify that valid tools were still registered - assert.Contains(t, output, "Running KAgent Tools Server") - assert.Contains(t, output, "k8s") - - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestConcurrentToolExecution tests concurrent tool execution -func TestConcurrentToolExecution(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8088, - Tools: []string{"utils", "k8s"}, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Create multiple concurrent requests - var wg sync.WaitGroup - for i := 0; i < 10; i++ { - wg.Add(1) - go func(id int) { - defer wg.Done() - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) - require.NoError(t, err, "Concurrent request %d should succeed", id) - assert.Equal(t, http.StatusOK, resp.StatusCode) - resp.Body.Close() - }(i) - } - - wg.Wait() - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestServerErrorHandling tests server's error handling capabilities -func TestServerErrorHandling(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8089, - Tools: []string{"utils"}, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Test malformed request - req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/nonexistent", config.Port), strings.NewReader("invalid json")) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - resp.Body.Close() - - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestServerMetricsEndpoint tests the metrics endpoint functionality -func TestServerMetricsEndpoint(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8090, - Tools: []string{"utils"}, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Test metrics endpoint - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/metrics", config.Port)) - require.NoError(t, err, "Metrics endpoint should be accessible") - assert.Equal(t, http.StatusOK, resp.StatusCode) - - // Read and verify metrics content - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - resp.Body.Close() - - metricsContent := string(body) - assert.Contains(t, metricsContent, "go_") - assert.Contains(t, metricsContent, "process_") - - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} - -// TestToolSpecificFunctionality tests specific functionality of registered tools -func TestToolSpecificFunctionality(t *testing.T) { - ctx := context.Background() - - config := TestServerConfig{ - Port: 8091, - Tools: []string{"utils", "k8s"}, - Stdio: false, - Timeout: 30 * time.Second, - } - - server := NewTestServer(config) - err := server.Start(ctx, config) - require.NoError(t, err, "Server should start successfully") - - // Wait for server to be ready - time.Sleep(3 * time.Second) - - // Test utils tool endpoint - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) - require.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - resp.Body.Close() - - // Verify response format matches expected OK response - assert.Equal(t, "OK", string(body), "Should return OK response") - - err = server.Stop() - require.NoError(t, err, "Server should stop gracefully") -} diff --git a/go.mod b/go.mod index 220ea7f2..cde53c82 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,16 @@ module github.com/kagent-dev/tools go 1.24.5 require ( - github.com/go-logr/logr v1.4.3 - github.com/go-logr/stdr v1.2.2 github.com/joho/godotenv v1.5.1 github.com/mark3labs/mcp-go v0.32.0 + github.com/onsi/ginkgo/v2 v2.23.4 + github.com/onsi/gomega v1.37.0 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.38.0 github.com/tmc/langchaingo v0.1.13 go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 go.opentelemetry.io/otel/metric v1.37.0 @@ -19,25 +21,69 @@ require ( ) require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/docker/docker v28.2.2+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.6 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.5 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.9.2 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect go.opentelemetry.io/proto/otlp v1.7.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + golang.org/x/crypto v0.39.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect diff --git a/go.sum b/go.sum index 3a1cf490..9966e3ec 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,46 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= +github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -14,11 +48,20 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= @@ -27,33 +70,94 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8= github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= +github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= +github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= @@ -74,14 +178,55 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= @@ -93,7 +238,10 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/helm/kagent-tools/Chart-template.yaml b/helm/kagent-tools/Chart-template.yaml new file mode 100644 index 00000000..6fb1a689 --- /dev/null +++ b/helm/kagent-tools/Chart-template.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: kagent-tools +description: A Helm chart for kagent-tools, +type: application +version: ${VERSION} diff --git a/helm/kagent-tools/templates/NOTES.txt b/helm/kagent-tools/templates/NOTES.txt new file mode 100644 index 00000000..a5be260b --- /dev/null +++ b/helm/kagent-tools/templates/NOTES.txt @@ -0,0 +1,10 @@ +################################ To open kagent UI: ########################################### +# +# This is a Helm chart for Kagent Tools +# +# 1. Forward application port by running these commands in the terminal: +# kubectl -n {{ include "kagent.namespace" . }} port-forward service/{{ .Release.Name }} {{.Values.service.ports.tools.targetPort}}:{{.Values.service.ports.tools.port}} & +# +# 2. Then visit http://127.0.0.1:{{.Values.service.ports.tools.port}}/mcp to use MCP +# +############################################################################################### \ No newline at end of file diff --git a/helm/kagent-tools/templates/_helpers.tpl b/helm/kagent-tools/templates/_helpers.tpl new file mode 100644 index 00000000..7922df08 --- /dev/null +++ b/helm/kagent-tools/templates/_helpers.tpl @@ -0,0 +1,75 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "kagent.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "kagent.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- if not .Values.nameOverride }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "kagent.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "kagent.labels" -}} +helm.sh/chart: {{ include "kagent.chart" . }} +{{ include "kagent.selectorLabels" . }} +{{- if .Chart.Version }} +app.kubernetes.io/version: {{ .Chart.Version | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "kagent.selectorLabels" -}} +app.kubernetes.io/name: {{ include "kagent.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/*Default provider name*/}} +{{- define "kagent.defaultProviderName" -}} +{{ .Values.providers.default | default "openAI" | lower}} +{{- end }} + +{{/*Default model name*/}} +{{- define "kagent.defaultModelConfigName" -}} +default-model-config +{{- end }} + +{{/* +Expand the namespace of the release. +Allows overriding it for multi-namespace deployments in combined charts. +*/}} +{{- define "kagent.namespace" -}} +{{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} +{{- end }} + +{{/* +Watch namespaces - transforms list of namespaces cached by the controller into comma-separated string +Removes duplicates +*/}} +{{- define "kagent.watchNamespaces" -}} +{{- $nsSet := dict }} +{{- .Values.controller.watchNamespaces | default list | uniq | join "," }} +{{- end -}} diff --git a/helm/kagent-tools/templates/clusterrole.yaml b/helm/kagent-tools/templates/clusterrole.yaml new file mode 100644 index 00000000..cde9f45e --- /dev/null +++ b/helm/kagent-tools/templates/clusterrole.yaml @@ -0,0 +1,29 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "kagent.fullname" . }}-cluster-admin-role + labels: + {{- include "kagent.labels" . | nindent 4 }} +rules: +- apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] +- nonResourceURLs: ["*"] + verbs: ["*"] +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "kagent.fullname" . }}-read-role + labels: + {{- include "kagent.labels" . | nindent 4 }} +rules: + - apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] + - nonResourceURLs: ["*"] + verbs: + - get + - list + - watch \ No newline at end of file diff --git a/helm/kagent-tools/templates/clusterrolebinding.yaml b/helm/kagent-tools/templates/clusterrolebinding.yaml new file mode 100644 index 00000000..ee7d67e8 --- /dev/null +++ b/helm/kagent-tools/templates/clusterrolebinding.yaml @@ -0,0 +1,44 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "kagent.fullname" . }}-cluster-admin-rolebinding + labels: + {{- include "kagent.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "kagent.fullname" . }}-cluster-admin-role +subjects: +- kind: ServiceAccount + name: {{ include "kagent.fullname" . }} + namespace: {{ include "kagent.namespace" . }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "kagent.fullname" . }}-getter-rolebinding + labels: + {{- include "kagent.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "kagent.fullname" . }}-getter-role +subjects: +- kind: ServiceAccount + name: {{ include "kagent.fullname" . }} + namespace: {{ include "kagent.namespace" . }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "kagent.fullname" . }}-writer-rolebinding + labels: + {{- include "kagent.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "kagent.fullname" . }}-writer-role +subjects: +- kind: ServiceAccount + name: {{ include "kagent.fullname" . }} + namespace: {{ include "kagent.namespace" . }} \ No newline at end of file diff --git a/helm/kagent-tools/templates/deployment.yaml b/helm/kagent-tools/templates/deployment.yaml new file mode 100644 index 00000000..48ac0d77 --- /dev/null +++ b/helm/kagent-tools/templates/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "kagent.fullname" . }} + namespace: {{ include "kagent.namespace" . }} + labels: + {{- include "kagent.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "kagent.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "kagent.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + serviceAccountName: {{ include "kagent.fullname" . }} + containers: + - name: tools + command: + - /tool-server + args: + - "--port" + - "{{ .Values.service.ports.tools.targetPort }}" + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.tools.image.registry }}/{{ .Values.tools.image.repository }}:{{ coalesce .Values.global.tag .Values.tools.image.tag .Chart.Version }}" + imagePullPolicy: {{ .Values.tools.image.pullPolicy }} + resources: + {{- toYaml .Values.tools.resources | nindent 12 }} + env: + - name: KAGENT_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "kagent.fullname" . }}-openai + key: OPENAI_API_KEY + optional: true # if the secret is not found, the tool will not be available + - name: OTEL_TRACING_ENABLED + value: {{ .Values.otel.tracing.enabled | quote }} + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ .Values.otel.tracing.exporter.otlp.endpoint | quote }} + - name: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT + value: {{ .Values.otel.tracing.exporter.otlp.timeout | quote }} + - name: OTEL_EXPORTER_OTLP_TRACES_INSECURE + value: {{ .Values.otel.tracing.exporter.otlp.insecure | quote }} + {{- with .Values.tools.env }} + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http-tools + containerPort: {{ .Values.service.ports.tools.targetPort }} + protocol: TCP + readinessProbe: + tcpSocket: + port: http-tools + initialDelaySeconds: 15 + periodSeconds: 15 + diff --git a/helm/kagent-tools/templates/service.yaml b/helm/kagent-tools/templates/service.yaml new file mode 100644 index 00000000..55c7fd2b --- /dev/null +++ b/helm/kagent-tools/templates/service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "kagent.fullname" . }} + namespace: {{ include "kagent.namespace" . }} + labels: + {{- include "kagent.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.ports.tools.port }} + targetPort: {{ .Values.service.ports.tools.targetPort }} + {{- if eq .Values.service.type "NodePort" }} # Only used if service.type is NodePort + {{- if .Values.service.ports.tools.nodePort }} + nodePort: {{ .Values.service.ports.tools.nodePort | default "" }} + {{- end }} + {{- end }} + protocol: TCP + name: tools + selector: + {{- include "kagent.selectorLabels" . | nindent 4 }} diff --git a/helm/kagent-tools/templates/serviceaccount.yaml b/helm/kagent-tools/templates/serviceaccount.yaml new file mode 100644 index 00000000..b0b4c03d --- /dev/null +++ b/helm/kagent-tools/templates/serviceaccount.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "kagent.fullname" . }} + namespace: {{ include "kagent.namespace" . }} + labels: + {{- include "kagent.labels" . | nindent 4 }} \ No newline at end of file diff --git a/helm/kagent-tools/values.yaml b/helm/kagent-tools/values.yaml new file mode 100644 index 00000000..6d68f55d --- /dev/null +++ b/helm/kagent-tools/values.yaml @@ -0,0 +1,61 @@ +# Default values for kagent +replicaCount: 1 + +global: + tag: "" + +tools: + loglevel: "debug" + image: + registry: ghcr.io + repository: kagent-dev/kagent/tools + tag: "" + pullPolicy: IfNotPresent + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 1000m + memory: 512Mi + prometheus: + url: "prometheus.kagent.svc.cluster.local:9090" + username: "" + password: "" + grafana: # kubectl port-forward svc/grafana 3000:3000 + url: "http://grafana.kagent.svc.cluster.local:3000" + apiKey: "" + +service: + type: ClusterIP + ports: + tools: + port: 8084 + targetPort: 8084 + nodePort: # Only used if service.type is NodePort + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +otel: + tracing: + enabled: false + exporter: + otlp: + endpoint: http://host.docker.internal:4317 + timeout: 15 + insecure: true diff --git a/internal/commands/builder.go b/internal/commands/builder.go index a6bd8e6d..f3e75ce2 100644 --- a/internal/commands/builder.go +++ b/internal/commands/builder.go @@ -26,6 +26,7 @@ type CommandBuilder struct { labels map[string]string annotations map[string]string timeout time.Duration + useTimeout bool dryRun bool force bool wait bool @@ -42,9 +43,10 @@ func NewCommandBuilder(command string) *CommandBuilder { args: make([]string, 0), labels: make(map[string]string), annotations: make(map[string]string), - timeout: 30 * time.Second, + timeout: 60 * time.Second, + useTimeout: false, // Only enable timeout when explicitly requested validate: true, - cacheTTL: 5 * time.Minute, + cacheTTL: 1 * time.Minute, } } @@ -101,11 +103,13 @@ func (cb *CommandBuilder) WithContext(context string) *CommandBuilder { // WithKubeconfig sets the kubeconfig file func (cb *CommandBuilder) WithKubeconfig(kubeconfig string) *CommandBuilder { - if err := security.ValidateFilePath(kubeconfig); err != nil { - logger.Get().Error("Invalid kubeconfig path", "kubeconfig", kubeconfig, "error", err) - return cb + if kubeconfig != "" { + if err := security.ValidateFilePath(kubeconfig); err != nil { + logger.Get().Error("Invalid kubeconfig path", "kubeconfig", kubeconfig, "error", err) + return cb + } + cb.kubeconfig = kubeconfig } - cb.kubeconfig = kubeconfig return cb } @@ -160,6 +164,7 @@ func (cb *CommandBuilder) WithAnnotation(key, value string) *CommandBuilder { // WithTimeout sets the command timeout func (cb *CommandBuilder) WithTimeout(timeout time.Duration) *CommandBuilder { + cb.useTimeout = true cb.timeout = timeout return cb } @@ -248,11 +253,9 @@ func (cb *CommandBuilder) Build() (string, []string, error) { } } - // Add timeout only for commands that support it - if cb.timeout > 0 { - if cb.supportsTimeout() { - args = append(args, "--timeout", cb.timeout.String()) - } + // Add timeout when explicitly requested + if cb.timeout > 0 && cb.useTimeout { + args = append(args, "--timeout", cb.timeout.String()) } // Add dry run @@ -278,56 +281,6 @@ func (cb *CommandBuilder) Build() (string, []string, error) { return cb.command, args, nil } -// supportsTimeout checks if the command supports the --timeout flag -func (cb *CommandBuilder) supportsTimeout() bool { - // For kubectl, many commands support --timeout - if cb.command == "kubectl" { - if len(cb.args) == 0 { - return false - } - - // Check the first argument (subcommand) - subcommand := cb.args[0] - switch subcommand { - case "wait": - return true - case "delete": - // kubectl delete supports --timeout when waiting for deletion - return true - case "rollout": - // kubectl rollout status supports --timeout - if len(cb.args) > 1 && cb.args[1] == "status" { - return true - } - return false - case "apply": - // kubectl apply supports --timeout when used with --wait - return cb.wait - case "annotate", "label": - // kubectl annotate and label support --timeout - return true - case "create": - // kubectl create supports --timeout - return true - case "argo": - // kubectl argo rollouts commands support --timeout - if len(cb.args) > 1 && cb.args[1] == "rollouts" { - return true - } - return false - case "get": - // kubectl get supports --timeout for some operations - return false // Most get operations don't need timeout, they're read-only - default: - return false - } - } - - // For other commands (helm, istioctl, cilium), assume they support timeout - // unless we find specific cases where they don't - return true -} - // Execute runs the command func (cb *CommandBuilder) Execute(ctx context.Context) (string, error) { log := logger.WithContext(ctx) @@ -465,8 +418,7 @@ func (cb *CommandBuilder) executeCommand(ctx context.Context, command string, ar default: toolError = errors.NewCommandError(command, err) } - - return "", toolError + return string(output), toolError } return string(output), nil diff --git a/internal/commands/builder_test.go b/internal/commands/builder_test.go index e326a763..52dc0ecf 100644 --- a/internal/commands/builder_test.go +++ b/internal/commands/builder_test.go @@ -19,8 +19,8 @@ func TestNewCommandBuilder(t *testing.T) { assert.Empty(t, cb.output) assert.NotNil(t, cb.labels) assert.NotNil(t, cb.annotations) - assert.Equal(t, 30*time.Second, cb.timeout) - assert.Equal(t, 5*time.Minute, cb.cacheTTL) + assert.Equal(t, 60*time.Second, cb.timeout) + assert.Equal(t, 1*time.Minute, cb.cacheTTL) assert.True(t, cb.validate) assert.False(t, cb.cached) assert.False(t, cb.dryRun) @@ -562,8 +562,6 @@ func TestCommandBuilderExecuteWithoutCache(t *testing.T) { assert.Equal(t, "echo", command) assert.Contains(t, args, "hello") assert.Contains(t, args, "world") - assert.Contains(t, args, "--timeout") - assert.Contains(t, args, "30s") } func TestCommandBuilderExecuteWithCache(t *testing.T) { @@ -579,7 +577,5 @@ func TestCommandBuilderExecuteWithCache(t *testing.T) { assert.Equal(t, "echo", command) assert.Contains(t, args, "hello") assert.Contains(t, args, "world") - assert.Contains(t, args, "--timeout") - assert.Contains(t, args, "30s") assert.True(t, cb.cached) } diff --git a/internal/security/validation.go b/internal/security/validation.go index 6aadc383..f06bf476 100644 --- a/internal/security/validation.go +++ b/internal/security/validation.go @@ -102,10 +102,6 @@ func ValidateContainerImage(image string) error { // ValidateFilePath validates a file path for security func ValidateFilePath(path string) error { - if path == "" { - return ValidationError{Field: "path", Message: "cannot be empty"} - } - if len(path) > 4096 { return ValidationError{Field: "path", Message: "path too long"} } diff --git a/pkg/argo/argo.go b/pkg/argo/argo.go index a24e9780..7edea840 100644 --- a/pkg/argo/argo.go +++ b/pkg/argo/argo.go @@ -354,6 +354,27 @@ func handleCheckPluginLogs(ctx context.Context, request mcp.CallToolRequest) (*m return mcp.NewToolResultText(status.String()), nil } +func handleListRollouts(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + ns := mcp.ParseString(request, "namespace", "argo-rollouts") + tt := mcp.ParseString(request, "type", "rollouts") + + cmd := []string{"argo", "rollouts", "list", tt} + if ns != "" { + cmd = append(cmd, "-n", ns) + } + + output, err := runArgoRolloutCommand(ctx, cmd) + if err != nil { + return mcp.NewToolResultError("Error listing rollouts: " + err.Error()), nil + } + + if strings.HasPrefix(output, "Error") { + return mcp.NewToolResultText(output), nil + } + + return mcp.NewToolResultText(output), nil +} + func RegisterTools(s *server.MCPServer) { s.AddTool(mcp.NewTool("argo_verify_argo_rollouts_controller_install", mcp.WithDescription("Verify that the Argo Rollouts controller is installed and running"), @@ -365,6 +386,12 @@ func RegisterTools(s *server.MCPServer) { mcp.WithDescription("Verify that the kubectl Argo Rollouts plugin is installed"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_verify_kubectl_plugin_install", handleVerifyKubectlPluginInstall))) + s.AddTool(mcp.NewTool("argo_rollouts_list", + mcp.WithDescription("List rollouts or experiments"), + mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), + mcp.WithString("type", mcp.Description("What to list: rollouts or experiments"), mcp.DefaultString("rollouts")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_rollouts_list", handleListRollouts))) + s.AddTool(mcp.NewTool("argo_promote_rollout", mcp.WithDescription("Promote a paused rollout to the next step"), mcp.WithString("rollout_name", mcp.Description("The name of the rollout to promote"), mcp.Required()), diff --git a/pkg/argo/argo_test.go b/pkg/argo/argo_test.go index 0f90c39c..3af620f2 100644 --- a/pkg/argo/argo_test.go +++ b/pkg/argo/argo_test.go @@ -30,7 +30,7 @@ func TestHandlePromoteRollout(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" promoted` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -43,23 +43,20 @@ func TestHandlePromoteRollout(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result) assert.False(t, result.IsError) - - // Verify the expected output - content := getResultText(result) - assert.Contains(t, content, "promoted") + assert.Contains(t, getResultText(result), "promoted") // Verify the correct command was called callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "promote", "myapp", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "promote", "myapp"}, callLog[0].Args) }) t.Run("promote rollout with namespace", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" promoted` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "-n", "production", "myapp", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "-n", "production", "myapp"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -77,14 +74,14 @@ func TestHandlePromoteRollout(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "promote", "-n", "production", "myapp", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "promote", "-n", "production", "myapp"}, callLog[0].Args) }) t.Run("promote rollout with full flag", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" fully promoted` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp", "--full", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp", "--full"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -98,11 +95,11 @@ func TestHandlePromoteRollout(t *testing.T) { assert.NoError(t, err) assert.False(t, result.IsError) - // Verify the correct command was called with --full flag + // Verify the correct command was called with full flag callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "promote", "myapp", "--full", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "promote", "myapp", "--full"}, callLog[0].Args) }) t.Run("missing required parameters", func(t *testing.T) { @@ -126,7 +123,7 @@ func TestHandlePromoteRollout(t *testing.T) { t.Run("kubectl command failure", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp", "--timeout", "30s"}, "", assert.AnError) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "promote", "myapp"}, "", assert.AnError) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -148,7 +145,7 @@ func TestHandlePauseRollout(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" paused` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "pause", "myapp", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "pause", "myapp"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -170,14 +167,14 @@ func TestHandlePauseRollout(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "pause", "myapp", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "pause", "myapp"}, callLog[0].Args) }) t.Run("pause rollout with namespace", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" paused` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "pause", "-n", "production", "myapp", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "pause", "-n", "production", "myapp"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -195,7 +192,7 @@ func TestHandlePauseRollout(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "pause", "-n", "production", "myapp", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "pause", "-n", "production", "myapp"}, callLog[0].Args) }) t.Run("missing required parameters", func(t *testing.T) { @@ -224,7 +221,7 @@ func TestHandleSetRolloutImage(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" image updated` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "set", "image", "myapp", "nginx:latest", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "set", "image", "myapp", "nginx:latest"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -247,14 +244,14 @@ func TestHandleSetRolloutImage(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "set", "image", "myapp", "nginx:latest", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "set", "image", "myapp", "nginx:latest"}, callLog[0].Args) }) t.Run("set rollout image with namespace", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `rollout "myapp" image updated` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "set", "image", "myapp", "nginx:1.20", "-n", "production", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "set", "image", "myapp", "nginx:1.20", "-n", "production"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -273,7 +270,7 @@ func TestHandleSetRolloutImage(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "set", "image", "myapp", "nginx:1.20", "-n", "production", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "set", "image", "myapp", "nginx:1.20", "-n", "production"}, callLog[0].Args) }) t.Run("missing rollout_name parameter", func(t *testing.T) { @@ -370,7 +367,7 @@ func TestHandleVerifyGatewayPlugin(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `gateway-api-plugin not found` - mock.AddCommandString("kubectl", []string{"get", "configmap", "argo-rollouts-config", "-n", "argo-rollouts", "-o", "yaml", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"get", "configmap", "argo-rollouts-config", "-n", "argo-rollouts", "-o", "yaml"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -397,7 +394,7 @@ func TestHandleVerifyGatewayPlugin(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `gateway-api-plugin-abc123` - mock.AddCommandString("kubectl", []string{"get", "configmap", "argo-rollouts-config", "-n", "custom-namespace", "-o", "yaml", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"get", "configmap", "argo-rollouts-config", "-n", "custom-namespace", "-o", "yaml"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -426,7 +423,7 @@ func TestHandleVerifyArgoRolloutsControllerInstall(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `argo-rollouts-controller-manager-abc123` - mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app.kubernetes.io/name=argo-rollouts", "-n", "argo-rollouts", "-o", "jsonpath={.items[*].metadata.name}", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app.kubernetes.io/name=argo-rollouts", "-n", "argo-rollouts", "-o", "jsonpath={.items[*].metadata.name}"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -447,7 +444,7 @@ func TestHandleVerifyArgoRolloutsControllerInstall(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `argo-rollouts-controller-manager-abc123` - mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app.kubernetes.io/name=argo-rollouts", "-n", "custom-argo", "-o", "jsonpath={.items[*].metadata.name}", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app.kubernetes.io/name=argo-rollouts", "-n", "custom-argo", "-o", "jsonpath={.items[*].metadata.name}"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -472,7 +469,7 @@ func TestHandleVerifyArgoRolloutsControllerInstall(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `argo-rollouts-controller-manager-abc123` - mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app=custom-rollouts", "-n", "argo-rollouts", "-o", "jsonpath={.items[*].metadata.name}", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"get", "pods", "-l", "app=custom-rollouts", "-n", "argo-rollouts", "-o", "jsonpath={.items[*].metadata.name}"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -500,25 +497,25 @@ func TestHandleVerifyKubectlPluginInstall(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `kubectl-argo-rollouts` - mock.AddCommandString("kubectl", []string{"argo", "rollouts", "version", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"argo", "rollouts", "version"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} result, err := handleVerifyKubectlPluginInstall(ctx, request) assert.NoError(t, err) - assert.NotNil(t, result) + assert.False(t, result.IsError) - // Verify kubectl command was called + // Verify the correct command was called callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "kubectl", callLog[0].Command) - assert.Equal(t, []string{"argo", "rollouts", "version", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"argo", "rollouts", "version"}, callLog[0].Args) }) t.Run("kubectl plugin command failure", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("kubectl", []string{"plugin", "list", "--timeout", "30s"}, "", assert.AnError) + mock.AddCommandString("kubectl", []string{"plugin", "list"}, "", assert.AnError) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} diff --git a/pkg/cilium/cilium_test.go b/pkg/cilium/cilium_test.go index 866de5d7..b7827de4 100644 --- a/pkg/cilium/cilium_test.go +++ b/pkg/cilium/cilium_test.go @@ -23,8 +23,8 @@ func TestRegisterCiliumTools(t *testing.T) { func TestHandleCiliumStatusAndVersion(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"status", "--timeout", "30s"}, "Cilium status: OK", nil) - mock.AddCommandString("cilium", []string{"version", "--timeout", "30s"}, "cilium version 1.14.0", nil) + mock.AddCommandString("cilium", []string{"status"}, "Cilium status: OK", nil) + mock.AddCommandString("cilium", []string{"version"}, "cilium version 1.14.0", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -49,8 +49,8 @@ func TestHandleCiliumStatusAndVersion(t *testing.T) { func TestHandleCiliumStatusAndVersionError(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"status", "--timeout", "30s"}, "", errors.New("command failed")) - mock.AddCommandString("cilium", []string{"version", "--timeout", "30s"}, "cilium version 1.14.0", nil) + mock.AddCommandString("cilium", []string{"status"}, "", errors.New("command failed")) + mock.AddCommandString("cilium", []string{"version"}, "cilium version 1.14.0", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -64,7 +64,7 @@ func TestHandleCiliumStatusAndVersionError(t *testing.T) { func TestHandleInstallCilium(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"install", "--timeout", "30s"}, "✓ Cilium was successfully installed!", nil) + mock.AddCommandString("cilium", []string{"install"}, "✓ Cilium was successfully installed!", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -78,7 +78,7 @@ func TestHandleInstallCilium(t *testing.T) { func TestHandleUninstallCilium(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"uninstall", "--timeout", "30s"}, "✓ Cilium was successfully uninstalled!", nil) + mock.AddCommandString("cilium", []string{"uninstall"}, "✓ Cilium was successfully uninstalled!", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -92,7 +92,7 @@ func TestHandleUninstallCilium(t *testing.T) { func TestHandleUpgradeCilium(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"upgrade", "--timeout", "30s"}, "✓ Cilium was successfully upgraded!", nil) + mock.AddCommandString("cilium", []string{"upgrade"}, "✓ Cilium was successfully upgraded!", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -108,7 +108,7 @@ func TestHandleConnectToRemoteCluster(t *testing.T) { t.Run("success", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"clustermesh", "connect", "--destination-cluster", "my-cluster", "--timeout", "30s"}, "✓ Connected to cluster my-cluster!", nil) + mock.AddCommandString("cilium", []string{"clustermesh", "connect", "--destination-cluster", "my-cluster"}, "✓ Connected to cluster my-cluster!", nil) ctx = cmd.WithShellExecutor(ctx, mock) req := mcp.CallToolRequest{ Params: mcp.CallToolParams{ @@ -144,7 +144,7 @@ func TestHandleDisconnectFromRemoteCluster(t *testing.T) { t.Run("success", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"clustermesh", "disconnect", "--destination-cluster", "my-cluster", "--timeout", "30s"}, "✓ Disconnected from cluster my-cluster!", nil) + mock.AddCommandString("cilium", []string{"clustermesh", "disconnect", "--destination-cluster", "my-cluster"}, "✓ Disconnected from cluster my-cluster!", nil) ctx = cmd.WithShellExecutor(ctx, mock) req := mcp.CallToolRequest{ Params: mcp.CallToolParams{ @@ -178,7 +178,7 @@ func TestHandleDisconnectFromRemoteCluster(t *testing.T) { func TestHandleEnableHubble(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"hubble", "enable", "--timeout", "30s"}, "✓ Hubble was successfully enabled!", nil) + mock.AddCommandString("cilium", []string{"hubble", "enable"}, "✓ Hubble was successfully enabled!", nil) ctx = cmd.WithShellExecutor(ctx, mock) req := mcp.CallToolRequest{ Params: mcp.CallToolParams{ @@ -198,7 +198,7 @@ func TestHandleEnableHubble(t *testing.T) { func TestHandleDisableHubble(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"hubble", "disable", "--timeout", "30s"}, "✓ Hubble was successfully disabled!", nil) + mock.AddCommandString("cilium", []string{"hubble", "disable"}, "✓ Hubble was successfully disabled!", nil) ctx = cmd.WithShellExecutor(ctx, mock) req := mcp.CallToolRequest{ Params: mcp.CallToolParams{ @@ -217,7 +217,7 @@ func TestHandleDisableHubble(t *testing.T) { func TestHandleListBGPPeers(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"bgp", "peers", "--timeout", "30s"}, "listing BGP peers", nil) + mock.AddCommandString("cilium", []string{"bgp", "peers"}, "listing BGP peers", nil) ctx = cmd.WithShellExecutor(ctx, mock) result, err := handleListBGPPeers(ctx, mcp.CallToolRequest{}) require.NoError(t, err) @@ -229,7 +229,7 @@ func TestHandleListBGPPeers(t *testing.T) { func TestHandleListBGPRoutes(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"bgp", "routes", "--timeout", "30s"}, "listing BGP routes", nil) + mock.AddCommandString("cilium", []string{"bgp", "routes"}, "listing BGP routes", nil) ctx = cmd.WithShellExecutor(ctx, mock) result, err := handleListBGPRoutes(ctx, mcp.CallToolRequest{}) require.NoError(t, err) @@ -242,7 +242,7 @@ func TestRunCiliumCliWithContext(t *testing.T) { ctx := context.Background() t.Run("success", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"test", "--timeout", "30s"}, "success", nil) + mock.AddCommandString("cilium", []string{"test"}, "success", nil) ctx = cmd.WithShellExecutor(ctx, mock) result, err := runCiliumCliWithContext(ctx, "test") require.NoError(t, err) @@ -250,7 +250,7 @@ func TestRunCiliumCliWithContext(t *testing.T) { }) t.Run("error", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("cilium", []string{"test", "--timeout", "30s"}, "", fmt.Errorf("test error")) + mock.AddCommandString("cilium", []string{"test"}, "", fmt.Errorf("test error")) ctx = cmd.WithShellExecutor(ctx, mock) _, err := runCiliumCliWithContext(ctx, "test") require.Error(t, err) diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 06a9ac8a..0bf1a4c8 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/kagent-dev/tools/internal/commands" "github.com/kagent-dev/tools/internal/errors" @@ -88,10 +89,18 @@ func handleHelmListReleases(ctx context.Context, request mcp.CallToolRequest) (* func runHelmCommand(ctx context.Context, args []string) (string, error) { kubeconfigPath := utils.GetKubeconfig() - result, err := commands.NewCommandBuilder("helm"). + + // Add timeout for helm upgrade commands + cmdBuilder := commands.NewCommandBuilder("helm"). WithArgs(args...). - WithKubeconfig(kubeconfigPath). - Execute(ctx) + WithKubeconfig(kubeconfigPath) + + // Only add timeout for upgrade commands + if len(args) > 0 && args[0] == "upgrade" { + cmdBuilder = cmdBuilder.WithTimeout(30 * time.Second) + } + + result, err := cmdBuilder.Execute(ctx) if err != nil { if toolErr, ok := err.(*errors.ToolError); ok { diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go index 3848de2a..28dca31a 100644 --- a/pkg/helm/helm_test.go +++ b/pkg/helm/helm_test.go @@ -18,106 +18,105 @@ func TestRegisterTools(t *testing.T) { // Test Helm List Releases func TestHandleHelmListReleases(t *testing.T) { - t.Run("basic list releases", func(t *testing.T) { - mock := cmd.NewMockShellExecutor() - expectedOutput := `NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -app1 default 1 2023-01-01 12:00:00.000000000 +0000 UTC deployed myapp-1.0.0 1.0.0 -app2 kube-system 2 2023-01-02 12:00:00.000000000 +0000 UTC deployed system-2.0.0 2.0.0` - - mock.AddCommandString("helm", []string{"list", "--timeout", "30s"}, expectedOutput, nil) - ctx := cmd.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - result, err := handleHelmListReleases(ctx, request) - - assert.NoError(t, err) - assert.NotNil(t, result) - assert.False(t, result.IsError) - - // Verify the expected output - content := getResultText(result) - assert.Contains(t, content, "app1") - assert.Contains(t, content, "app2") - - // Verify the correct command was called - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"list", "--timeout", "30s"}, callLog[0].Args) - }) - - t.Run("list releases with namespace", func(t *testing.T) { - mock := cmd.NewMockShellExecutor() - mock.AddCommandString("helm", []string{"list", "-n", "production", "--timeout", "30s"}, "production releases", nil) - ctx := cmd.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "namespace": "production", - } - - result, err := handleHelmListReleases(ctx, request) - - assert.NoError(t, err) - assert.False(t, result.IsError) - - // Verify the correct command was called with namespace - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"list", "-n", "production", "--timeout", "30s"}, callLog[0].Args) - }) - - t.Run("list releases with all namespaces", func(t *testing.T) { - mock := cmd.NewMockShellExecutor() - mock.AddCommandString("helm", []string{"list", "-A", "--timeout", "30s"}, "all namespaces releases", nil) - ctx := cmd.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "all_namespaces": "true", - } - - result, err := handleHelmListReleases(ctx, request) - - assert.NoError(t, err) - assert.False(t, result.IsError) - - // Verify the correct command was called with -A flag - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"list", "-A", "--timeout", "30s"}, callLog[0].Args) - }) - - t.Run("list releases with multiple flags", func(t *testing.T) { - mock := cmd.NewMockShellExecutor() - mock.AddCommandString("helm", []string{"list", "-A", "-a", "--failed", "-o", "json", "--timeout", "30s"}, `[{"name":"failed-app","status":"failed"}]`, nil) - ctx := cmd.WithShellExecutor(context.Background(), mock) - - request := mcp.CallToolRequest{} - request.Params.Arguments = map[string]interface{}{ - "all_namespaces": "true", - "all": "true", - "failed": "true", - "output": "json", - } - - result, err := handleHelmListReleases(ctx, request) - - assert.NoError(t, err) - assert.False(t, result.IsError) + tests := []struct { + name string + args map[string]interface{} + expectedArgs []string + expectedOutput string + expectError bool + }{ + { + name: "basic_list_releases", + args: map[string]interface{}{}, + expectedArgs: []string{"list"}, + expectedOutput: `NAME NAMESPACE REVISION STATUS CHART +app1 default 1 deployed my-chart-1.0.0 +app2 default 2 deployed my-chart-2.0.0`, + expectError: false, + }, + { + name: "list_releases_with_namespace", + args: map[string]interface{}{ + "namespace": "production", + }, + expectedArgs: []string{"list", "-n", "production"}, + expectedOutput: `NAME NAMESPACE REVISION STATUS CHART +prod-app production 1 deployed my-chart-1.0.0`, + expectError: false, + }, + { + name: "list_releases_with_all_namespaces", + args: map[string]interface{}{ + "all_namespaces": "true", + }, + expectedArgs: []string{"list", "-A"}, + expectedOutput: `NAME NAMESPACE REVISION STATUS CHART +app1 default 1 deployed my-chart-1.0.0 +prod-app production 1 deployed my-chart-1.0.0`, + expectError: false, + }, + { + name: "list_releases_with_multiple_flags", + args: map[string]interface{}{ + "all_namespaces": "true", + "all": "true", + "failed": "true", + "output": "json", + }, + expectedArgs: []string{"list", "-A", "-a", "--failed", "-o", "json"}, + expectedOutput: `[ + { + "name": "app1", + "namespace": "default", + "revision": "1", + "status": "deployed" + } +]`, + expectError: false, + }, + } - // Verify the correct command was called with multiple flags - callLog := mock.GetCallLog() - require.Len(t, callLog, 1) - assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"list", "-A", "-a", "--failed", "-o", "json", "--timeout", "30s"}, callLog[0].Args) - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + mock.AddCommandString("helm", tt.expectedArgs, tt.expectedOutput, nil) + ctx := cmd.WithShellExecutor(context.Background(), mock) + + request := mcp.CallToolRequest{} + request.Params.Arguments = tt.args + + result, err := handleHelmListReleases(ctx, request) + + assert.NoError(t, err) + assert.False(t, result.IsError) + + // Verify the expected output + content := getResultText(result) + if tt.name == "basic_list_releases" { + assert.Contains(t, content, "app1") + assert.Contains(t, content, "app2") + } else if tt.name == "list_releases_with_namespace" { + assert.Contains(t, content, "prod-app") + assert.Contains(t, content, "production") + } else if tt.name == "list_releases_with_all_namespaces" { + assert.Contains(t, content, "app1") + assert.Contains(t, content, "prod-app") + } else if tt.name == "list_releases_with_multiple_flags" { + assert.Contains(t, content, "app1") + assert.Contains(t, content, "default") + } + + // Verify the correct command was called + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Equal(t, "helm", callLog[0].Command) + assert.Equal(t, tt.expectedArgs, callLog[0].Args) + }) + } t.Run("helm command failure", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("helm", []string{"list", "--timeout", "30s"}, "", assert.AnError) + mock.AddCommandString("helm", []string{"list"}, "", assert.AnError) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -139,7 +138,7 @@ CHART: myapp-1.0.0 VALUES: replicaCount: 3` - mock.AddCommandString("helm", []string{"get", "all", "myapp", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("helm", []string{"get", "all", "myapp", "-n", "default"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -158,12 +157,12 @@ replicaCount: 3` callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"get", "all", "myapp", "-n", "default", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"get", "all", "myapp", "-n", "default"}, callLog[0].Args) }) t.Run("get release values only", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("helm", []string{"get", "values", "myapp", "-n", "default", "--timeout", "30s"}, "replicaCount: 3", nil) + mock.AddCommandString("helm", []string{"get", "values", "myapp", "-n", "default"}, "replicaCount: 3", nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -182,7 +181,7 @@ replicaCount: 3` callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"get", "values", "myapp", "-n", "default", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"get", "values", "myapp", "-n", "default"}, callLog[0].Args) }) t.Run("missing required parameters", func(t *testing.T) { @@ -318,7 +317,7 @@ func TestHandleHelmUninstall(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `release "myapp" uninstalled` - mock.AddCommandString("helm", []string{"uninstall", "myapp", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("helm", []string{"uninstall", "myapp", "-n", "default"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -330,6 +329,7 @@ func TestHandleHelmUninstall(t *testing.T) { result, err := handleHelmUninstall(ctx, request) assert.NoError(t, err) + assert.NotNil(t, result) assert.False(t, result.IsError) assert.Contains(t, getResultText(result), "uninstalled") @@ -337,13 +337,14 @@ func TestHandleHelmUninstall(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"uninstall", "myapp", "-n", "default", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"uninstall", "myapp", "-n", "default"}, callLog[0].Args) }) t.Run("uninstall with options", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - expectedArgs := []string{"uninstall", "myapp", "-n", "production", "--dry-run", "--wait", "--timeout", "30s"} - mock.AddCommandString("helm", expectedArgs, "dry run uninstall", nil) + expectedOutput := `release "myapp" uninstalled` + + mock.AddCommandString("helm", []string{"uninstall", "myapp", "-n", "production", "--dry-run", "--wait"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -363,7 +364,7 @@ func TestHandleHelmUninstall(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, expectedArgs, callLog[0].Args) + assert.Equal(t, []string{"uninstall", "myapp", "-n", "production", "--dry-run", "--wait"}, callLog[0].Args) }) t.Run("missing required parameters for uninstall", func(t *testing.T) { @@ -403,7 +404,7 @@ func TestHandleHelmRepoAdd(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `"my-repo" has been added to your repositories` - mock.AddCommandString("helm", []string{"repo", "add", "my-repo", "https://charts.example.com/", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("helm", []string{"repo", "add", "my-repo", "https://charts.example.com/"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -422,7 +423,7 @@ func TestHandleHelmRepoAdd(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"repo", "add", "my-repo", "https://charts.example.com/", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"repo", "add", "my-repo", "https://charts.example.com/"}, callLog[0].Args) }) t.Run("missing required parameters for repo add", func(t *testing.T) { @@ -451,9 +452,10 @@ func TestHandleHelmRepoUpdate(t *testing.T) { t.Run("basic repo update", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `Hang tight while we grab the latest from your chart repositories... -...Successfully got an update from the "my-repo" chart repository` +...Successfully got an update from the "stable" chart repository +Update Complete. ⎈Happy Helming!⎈` - mock.AddCommandString("helm", []string{"repo", "update", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("helm", []string{"repo", "update"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(context.Background(), mock) request := mcp.CallToolRequest{} @@ -467,7 +469,7 @@ func TestHandleHelmRepoUpdate(t *testing.T) { callLog := mock.GetCallLog() require.Len(t, callLog, 1) assert.Equal(t, "helm", callLog[0].Command) - assert.Equal(t, []string{"repo", "update", "--timeout", "30s"}, callLog[0].Args) + assert.Equal(t, []string{"repo", "update"}, callLog[0].Args) }) } diff --git a/pkg/istio/istio_test.go b/pkg/istio/istio_test.go index 02efbee4..d2503c9d 100644 --- a/pkg/istio/istio_test.go +++ b/pkg/istio/istio_test.go @@ -21,7 +21,7 @@ func TestHandleIstioProxyStatus(t *testing.T) { t.Run("basic proxy status", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"proxy-status", "--timeout", "30s"}, "Proxy status output", nil) + mock.AddCommandString("istioctl", []string{"proxy-status"}, "Proxy status output", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -34,7 +34,7 @@ func TestHandleIstioProxyStatus(t *testing.T) { t.Run("proxy status with namespace", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"proxy-status", "-n", "istio-system", "--timeout", "30s"}, "Proxy status output", nil) + mock.AddCommandString("istioctl", []string{"proxy-status", "-n", "istio-system"}, "Proxy status output", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -52,7 +52,7 @@ func TestHandleIstioProxyStatus(t *testing.T) { t.Run("proxy status with pod name", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"proxy-status", "-n", "default", "test-pod", "--timeout", "30s"}, "Proxy status output", nil) + mock.AddCommandString("istioctl", []string{"proxy-status", "-n", "default", "test-pod"}, "Proxy status output", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -83,7 +83,7 @@ func TestHandleIstioProxyConfig(t *testing.T) { t.Run("proxy config with pod name", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"proxy-config", "all", "test-pod", "--timeout", "30s"}, "Proxy config output", nil) + mock.AddCommandString("istioctl", []string{"proxy-config", "all", "test-pod"}, "Proxy config output", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -101,7 +101,7 @@ func TestHandleIstioProxyConfig(t *testing.T) { t.Run("proxy config with namespace", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"proxy-config", "cluster", "test-pod.default", "--timeout", "30s"}, "Proxy config output", nil) + mock.AddCommandString("istioctl", []string{"proxy-config", "cluster", "test-pod.default"}, "Proxy config output", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -125,7 +125,7 @@ func TestHandleIstioInstall(t *testing.T) { t.Run("install with default profile", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"install", "--set", "profile=default", "-y", "--timeout", "30s"}, "Install completed", nil) + mock.AddCommandString("istioctl", []string{"install", "--set", "profile=default", "-y"}, "Install completed", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -138,7 +138,7 @@ func TestHandleIstioInstall(t *testing.T) { t.Run("install with custom profile", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"install", "--set", "profile=demo", "-y", "--timeout", "30s"}, "Install completed", nil) + mock.AddCommandString("istioctl", []string{"install", "--set", "profile=demo", "-y"}, "Install completed", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -159,7 +159,7 @@ func TestHandleIstioGenerateManifest(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"manifest", "generate", "--set", "profile=minimal", "--timeout", "30s"}, "Generated manifest", nil) + mock.AddCommandString("istioctl", []string{"manifest", "generate", "--set", "profile=minimal"}, "Generated manifest", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -180,7 +180,7 @@ func TestHandleIstioAnalyzeClusterConfiguration(t *testing.T) { t.Run("analyze all namespaces", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"analyze", "-A", "--timeout", "30s"}, "Analysis output", nil) + mock.AddCommandString("istioctl", []string{"analyze", "-A"}, "Analysis output", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -198,7 +198,7 @@ func TestHandleIstioAnalyzeClusterConfiguration(t *testing.T) { t.Run("analyze specific namespace", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"analyze", "-n", "default", "--timeout", "30s"}, "Analysis output", nil) + mock.AddCommandString("istioctl", []string{"analyze", "-n", "default"}, "Analysis output", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -220,7 +220,7 @@ func TestHandleIstioVersion(t *testing.T) { t.Run("version full", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"version", "--timeout", "30s"}, "Version output", nil) + mock.AddCommandString("istioctl", []string{"version"}, "Version output", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -233,7 +233,7 @@ func TestHandleIstioVersion(t *testing.T) { t.Run("version short", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"version", "--short", "--timeout", "30s"}, "1.18.0", nil) + mock.AddCommandString("istioctl", []string{"version", "--short"}, "1.18.0", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -254,7 +254,7 @@ func TestHandleIstioRemoteClusters(t *testing.T) { ctx := context.Background() mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"remote-clusters", "--timeout", "30s"}, "Remote clusters output", nil) + mock.AddCommandString("istioctl", []string{"remote-clusters"}, "Remote clusters output", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -270,7 +270,7 @@ func TestHandleWaypointList(t *testing.T) { t.Run("list waypoints in all namespaces", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"waypoint", "list", "-A", "--timeout", "30s"}, "Waypoints list", nil) + mock.AddCommandString("istioctl", []string{"waypoint", "list", "-A"}, "Waypoints list", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -288,7 +288,7 @@ func TestHandleWaypointList(t *testing.T) { t.Run("list waypoints in a specific namespace", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"waypoint", "list", "-n", "default", "--timeout", "30s"}, "Waypoints list", nil) + mock.AddCommandString("istioctl", []string{"waypoint", "list", "-n", "default"}, "Waypoints list", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -310,7 +310,7 @@ func TestHandleWaypointGenerate(t *testing.T) { t.Run("generate waypoint with namespace", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"waypoint", "generate", "waypoint", "-n", "default", "--for", "all", "--timeout", "30s"}, "Generated waypoint", nil) + mock.AddCommandString("istioctl", []string{"waypoint", "generate", "waypoint", "-n", "default", "--for", "all"}, "Generated waypoint", nil) ctx = cmd.WithShellExecutor(ctx, mock) @@ -332,7 +332,7 @@ func TestHandleWaypointGenerate(t *testing.T) { func TestRunIstioCtl(t *testing.T) { t.Run("run istioctl with context", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("istioctl", []string{"version", "--timeout", "30s"}, "1.18.0", nil) + mock.AddCommandString("istioctl", []string{"version"}, "1.18.0", nil) ctx := cmd.WithShellExecutor(context.Background(), mock) result, err := runIstioCtl(ctx, []string{"version"}) diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index 6c29c9c1..e474df61 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -9,6 +9,7 @@ import ( "os" "slices" "strings" + "time" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -236,7 +237,7 @@ func (k *K8sTool) handleCheckServiceConnectivity(ctx context.Context, request mc } // Wait for pod to be ready - _, err = k.runKubectlCommand(ctx, "wait", "--for=condition=ready", "pod/"+podName, "-n", namespace, "--timeout=60s") + _, err = k.runKubectlCommandWithTimeout(ctx, 60*time.Second, "wait", "--for=condition=ready", "pod/"+podName, "-n", namespace) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to wait for curl pod: %v", err)), nil } @@ -541,6 +542,21 @@ func (k *K8sTool) runKubectlCommand(ctx context.Context, args ...string) (*mcp.C return mcp.NewToolResultText(output), nil } +// runKubectlCommandWithTimeout is a helper function to execute kubectl commands with a timeout +func (k *K8sTool) runKubectlCommandWithTimeout(ctx context.Context, timeout time.Duration, args ...string) (*mcp.CallToolResult, error) { + output, err := commands.NewCommandBuilder("kubectl"). + WithArgs(args...). + WithKubeconfig(k.kubeconfig). + WithTimeout(timeout). + Execute(ctx) + + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + return mcp.NewToolResultText(output), nil +} + // RegisterK8sTools registers all k8s tools with the MCP server func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { k8sTool := NewK8sToolWithConfig(kubeconfig, llm) diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go index a71e10fb..f3c53c4b 100644 --- a/pkg/k8s/k8s_test.go +++ b/pkg/k8s/k8s_test.go @@ -267,7 +267,7 @@ func TestHandleDeleteResource(t *testing.T) { t.Run("valid parameters", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment deleted` - mock.AddCommandString("kubectl", []string{"delete", "deployment", "test-deployment", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"delete", "deployment", "test-deployment", "-n", "default"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -315,9 +315,9 @@ func TestHandleCheckServiceConnectivity(t *testing.T) { // Mock the pod creation, wait, and exec commands using partial matchers mock.AddPartialMatcherString("kubectl", []string{"run", "*", "--image=curlimages/curl", "-n", "default", "--restart=Never", "--", "sleep", "3600"}, "pod/curl-test-123 created", nil) - mock.AddPartialMatcherString("kubectl", []string{"wait", "--for=condition=ready", "*", "-n", "default", "--timeout=60s", "--timeout", "30s"}, "pod/curl-test-123 condition met", nil) + mock.AddPartialMatcherString("kubectl", []string{"wait", "--for=condition=ready", "*", "-n", "default", "--timeout=60s"}, "pod/curl-test-123 condition met", nil) mock.AddPartialMatcherString("kubectl", []string{"exec", "*", "-n", "default", "--", "curl", "-s", "test-service.default.svc.cluster.local:80"}, "Connection successful", nil) - mock.AddPartialMatcherString("kubectl", []string{"delete", "pod", "*", "-n", "default", "--ignore-not-found", "--timeout", "30s"}, "pod deleted", nil) + mock.AddPartialMatcherString("kubectl", []string{"delete", "pod", "*", "-n", "default", "--ignore-not-found"}, "pod deleted", nil) ctx := cmd.WithShellExecutor(ctx, mock) @@ -774,7 +774,7 @@ func TestHandleAnnotateResource(t *testing.T) { t.Run("success", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment annotated` - mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1=value1", "key2=value2", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1=value1", "key2=value2", "-n", "default"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -826,7 +826,7 @@ func TestHandleLabelResource(t *testing.T) { t.Run("success", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment labeled` - mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env=prod", "version=1.0", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env=prod", "version=1.0", "-n", "default"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -878,7 +878,7 @@ func TestHandleRemoveAnnotation(t *testing.T) { t.Run("success", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment annotated` - mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1-", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1-", "-n", "default"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -930,7 +930,7 @@ func TestHandleRemoveLabel(t *testing.T) { t.Run("success", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment labeled` - mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env-", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env-", "-n", "default"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -982,7 +982,7 @@ func TestHandleCreateResourceFromURL(t *testing.T) { t.Run("success", func(t *testing.T) { mock := cmd.NewMockShellExecutor() expectedOutput := `deployment.apps/test-deployment created` - mock.AddCommandString("kubectl", []string{"create", "-f", "https://example.com/manifest.yaml", "-n", "default", "--timeout", "30s"}, expectedOutput, nil) + mock.AddCommandString("kubectl", []string{"create", "-f", "https://example.com/manifest.yaml", "-n", "default"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() diff --git a/reports/cve-report.tmpl b/reports/cve-report.tmpl new file mode 100644 index 00000000..4518291b --- /dev/null +++ b/reports/cve-report.tmpl @@ -0,0 +1,4 @@ +"Package","Version Installed","Vulnerability ID","Severity","Location","Version Fixed" +{{- range .Matches}} +"{{.Artifact.Name}}","{{.Artifact.Version}}","{{.Vulnerability.ID}}","{{.Vulnerability.Severity}}","{{ (index .Artifact.Locations 0).RealPath }}","{{.Vulnerability.Fix.Versions | join ".."}}" +{{- end}} \ No newline at end of file diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 00000000..81c57880 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,252 @@ +#!/bin/bash + +# exit on error +set -e + +#do not output commands +set +x + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +# Configuration +GITHUB_REPO="kagent-dev/tools" +BINARY_NAME="kagent-tools" +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" + +# Helper functions +log_info() { + echo -e "${BLUE}ℹ️ ${NC}$1" >&2 +} + +log_success() { + echo -e "${GREEN}✅ ${NC}$1" >&2 +} + +log_step() { + echo -e "${CYAN}🔄 ${NC}$1" >&2 +} + +log_step_complete() { + # Move cursor up one line and clear it, then print the completed message + echo -e "\033[1A\033[2K${GREEN}✅ ${NC}$1" >&2 +} + +log_warn() { + echo -e "${YELLOW}⚠️ ${NC}$1" >&2 +} + +log_error() { + echo -e "${RED}❌ ${NC}$1" >&2 +} + +log_header() { + echo -e "\n${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2 + echo -e "${BOLD}${CYAN} 🚀 kagent-tools Installer${NC}" >&2 + echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" >&2 +} + +# Check if command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Detect OS and architecture +detect_platform() { + local os + local arch + + # Detect OS + case "$(uname -s)" in + Darwin) + os="darwin" + ;; + Linux) + os="linux" + ;; + CYGWIN*|MINGW32*|MSYS*|MINGW*) + os="windows" + ;; + *) + log_error "Unsupported operating system: $(uname -s)" + exit 1 + ;; + esac + + # Detect architecture + case "$(uname -m)" in + x86_64|amd64) + arch="amd64" + ;; + arm64|aarch64) + arch="arm64" + ;; + armv7l) + arch="arm" + ;; + *) + log_error "Unsupported architecture: $(uname -m)" + exit 1 + ;; + esac + + echo "${os}-${arch}" +} + +# Get latest release version from GitHub +get_latest_version() { + log_step "Fetching latest version from GitHub..." + + if command_exists curl; then + local response=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest") + elif command_exists wget; then + local response=$(wget -qO- "https://api.github.com/repos/${GITHUB_REPO}/releases/latest") + else + log_error "Neither curl nor wget is available. Please install one of them." + exit 1 + fi + + # Extract version using basic tools (avoiding jq dependency) + local version=$(echo "$response" | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/') + + if [ -z "$version" ]; then + echo "" >&2 # Add newline before error + log_error "Failed to get latest version from GitHub API" + exit 1 + fi + + echo "$version" +} + +# Download binary +download_binary() { + local version="$1" + local platform="$2" + local download_url="https://github.com/${GITHUB_REPO}/releases/download/${version}/${BINARY_NAME}-${platform}" + local temp_file="/tmp/${BINARY_NAME}" + rm -rf "/tmp/${BINARY_NAME}" + + log_step "Downloading ${BINARY_NAME} ${BOLD}${version}${NC} for ${BOLD}${platform}${NC} from GitHub..." + + if command_exists curl; then + curl -sL -o "$temp_file" "$download_url" + elif command_exists wget; then + wget -q -O "$temp_file" "$download_url" + else + log_error "Neither curl nor wget is available. Please install one of them." + exit 1 + fi + + if [ ! -f "$temp_file" ]; then + echo "" >&2 # Add newline before error + log_error "Failed to download binary from $download_url" + exit 1 + fi + + echo "$temp_file" +} + +# Install binary +install_binary() { + local temp_file="$1" + local install_path="${INSTALL_DIR}/${BINARY_NAME}" + + log_step "Installing ${BINARY_NAME} to ${BOLD}${install_path}${NC}..." + + # Create install directory if it doesn't exist + if [ ! -d "$INSTALL_DIR" ]; then + echo -e "\n${BLUE}ℹ️ ${NC}📁 Creating install directory: ${INSTALL_DIR}" >&2 + mkdir -p "$INSTALL_DIR" + fi + + mv "$temp_file" "$install_path" + chmod +x "$install_path" + + # Cleanup + rm -f "$temp_file" +} + +# Verify installation +verify_installation() { + log_step "Verifying installation..." + + if command_exists "$BINARY_NAME"; then + local version=$("$BINARY_NAME" --version 2>/dev/null || echo "unknown") + log_step_complete "Installation verified! ${BINARY_NAME} is available in PATH" + echo -e "${BLUE}ℹ️ ${NC}🏷️ Version: ${BOLD}${version}${NC}" >&2 + else + echo "" >&2 # Add newline before warning + log_warn "${BINARY_NAME} is installed but not in PATH" + log_info "💡 Add this line to your shell profile (~/.bashrc, ~/.zshrc, etc.):" + echo -e "${CYAN} export PATH=\"${INSTALL_DIR}:\$PATH\"${NC}" >&2 + fi +} + +# Main installation function +main() { + log_header + + # Check prerequisites + log_step "Checking prerequisites..." + if ! command_exists curl && ! command_exists wget; then + echo "" >&2 # Add newline before error + log_error "This script requires either curl or wget to be installed." + exit 1 + fi + log_step_complete "Prerequisites check passed" + + # Detect platform + log_step "Detecting platform..." + local platform=$(detect_platform) + log_step_complete "Platform detected: ${BOLD}${platform}${NC}" + + # Get latest version + local version=$(get_latest_version) + log_step_complete "Found latest version: ${BOLD}${version}${NC}" + + # Download binary + local temp_file=$(download_binary "$version" "$platform") + log_step_complete "Binary downloaded successfully" + + # Install binary + install_binary "$temp_file" + log_step_complete "Binary installed successfully" + + # Verify installation + verify_installation + + echo -e "\n${BOLD}${GREEN}🎉 Installation completed successfully!${NC}" >&2 + echo -e "${GREEN} You can now use '${BOLD}kagent-tools${NC}${GREEN}' command.${NC}\n" >&2 +} + +# Handle command line arguments +case "${1:-}" in + -h|--help) + echo -e "${BOLD}${CYAN}🚀 kagent-tools Installer${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "\n${BOLD}USAGE:${NC}" + echo -e " $0 [OPTIONS]" + echo -e "\n${BOLD}DESCRIPTION:${NC}" + echo -e " Install kagent-tools from GitHub releases" + echo -e "\n${BOLD}OPTIONS:${NC}" + echo -e " -h, --help Show this help message" + echo -e "\n${BOLD}ENVIRONMENT VARIABLES:${NC}" + echo -e " INSTALL_DIR Installation directory (default: \$HOME/.local/bin)" + echo -e "\n${BOLD}EXAMPLES:${NC}" + echo -e " ${GREEN}$0${NC} # Install to \$HOME/.local/bin" + echo -e " ${GREEN}INSTALL_DIR=~/bin $0${NC} # Install to ~/bin" + echo "" + exit 0 + ;; + *) + main "$@" + ;; +esac + + diff --git a/scripts/kind/crd-argo.yaml b/scripts/kind/crd-argo.yaml new file mode 100644 index 00000000..23dff637 --- /dev/null +++ b/scripts/kind/crd-argo.yaml @@ -0,0 +1,16470 @@ +# This is an auto-generated file. DO NOT EDIT +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: analysisruns.argoproj.io +spec: + group: argoproj.io + names: + kind: AnalysisRun + listKind: AnalysisRunList + plural: analysisruns + shortNames: + - ar + singular: analysisrun + preserveUnknownFields: false + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: AnalysisRun status + jsonPath: .status.phase + name: Status + type: string + - description: Time since resource was created + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + metrics: + items: + properties: + consecutiveErrorLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + consecutiveSuccessLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + count: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + failureCondition: + type: string + failureLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + inconclusiveLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + initialDelay: + type: string + interval: + type: string + name: + type: string + provider: + properties: + cloudWatch: + properties: + interval: + type: string + metricDataQueries: + items: + properties: + expression: + type: string + id: + type: string + label: + type: string + metricStat: + properties: + metric: + properties: + dimensions: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + metricName: + type: string + namespace: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + stat: + type: string + unit: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + returnData: + type: boolean + type: object + type: array + required: + - metricDataQueries + type: object + datadog: + properties: + aggregator: + enum: + - avg + - min + - max + - sum + - last + - percentile + - mean + - l2norm + - area + type: string + apiVersion: + default: v1 + enum: + - v1 + - v2 + type: string + formula: + type: string + interval: + default: 5m + type: string + queries: + additionalProperties: + type: string + type: object + query: + type: string + secretRef: + properties: + name: + type: string + namespaced: + type: boolean + type: object + type: object + graphite: + properties: + address: + type: string + query: + type: string + type: object + influxdb: + properties: + profile: + type: string + query: + type: string + type: object + job: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + suspend: + type: boolean + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + items: + properties: + name: + type: string + source: + properties: + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + x-kubernetes-preserve-unknown-fields: true + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + required: + - spec + type: object + kayenta: + properties: + address: + type: string + application: + type: string + canaryConfigName: + type: string + configurationAccountName: + type: string + metricsAccountName: + type: string + scopes: + items: + properties: + controlScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + experimentScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + name: + type: string + required: + - controlScope + - experimentScope + - name + type: object + type: array + storageAccountName: + type: string + threshold: + properties: + marginal: + format: int64 + type: integer + pass: + format: int64 + type: integer + required: + - marginal + - pass + type: object + required: + - address + - application + - canaryConfigName + - configurationAccountName + - metricsAccountName + - scopes + - storageAccountName + - threshold + type: object + newRelic: + properties: + profile: + type: string + query: + type: string + timeout: + format: int64 + type: integer + required: + - query + type: object + plugin: + type: object + x-kubernetes-preserve-unknown-fields: true + prometheus: + properties: + address: + type: string + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + query: + type: string + rangeQuery: + properties: + end: + type: string + start: + type: string + step: + type: string + type: object + timeout: + format: int64 + type: integer + type: object + skywalking: + properties: + address: + type: string + interval: + type: string + query: + type: string + type: object + wavefront: + properties: + address: + type: string + query: + type: string + type: object + web: + properties: + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + body: + type: string + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + jsonBody: + type: object + x-kubernetes-preserve-unknown-fields: true + jsonPath: + type: string + method: + type: string + timeoutSeconds: + format: int64 + type: integer + url: + type: string + required: + - url + type: object + type: object + successCondition: + type: string + required: + - name + - provider + type: object + type: array + terminate: + type: boolean + ttlStrategy: + properties: + secondsAfterCompletion: + format: int32 + type: integer + secondsAfterFailure: + format: int32 + type: integer + secondsAfterSuccess: + format: int32 + type: integer + type: object + required: + - metrics + type: object + status: + properties: + completedAt: + format: date-time + type: string + dryRunSummary: + properties: + count: + format: int32 + type: integer + error: + format: int32 + type: integer + failed: + format: int32 + type: integer + inconclusive: + format: int32 + type: integer + successful: + format: int32 + type: integer + type: object + message: + type: string + metricResults: + items: + properties: + consecutiveError: + format: int32 + type: integer + consecutiveSuccess: + format: int32 + type: integer + count: + format: int32 + type: integer + dryRun: + type: boolean + error: + format: int32 + type: integer + failed: + format: int32 + type: integer + inconclusive: + format: int32 + type: integer + measurements: + items: + properties: + finishedAt: + format: date-time + type: string + message: + type: string + metadata: + additionalProperties: + type: string + type: object + phase: + type: string + resumeAt: + format: date-time + type: string + startedAt: + format: date-time + type: string + value: + type: string + required: + - phase + type: object + type: array + message: + type: string + metadata: + additionalProperties: + type: string + type: object + name: + type: string + phase: + type: string + successful: + format: int32 + type: integer + required: + - name + - phase + type: object + type: array + phase: + type: string + runSummary: + properties: + count: + format: int32 + type: integer + error: + format: int32 + type: integer + failed: + format: int32 + type: integer + inconclusive: + format: int32 + type: integer + successful: + format: int32 + type: integer + type: object + startedAt: + format: date-time + type: string + required: + - phase + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: analysistemplates.argoproj.io +spec: + group: argoproj.io + names: + kind: AnalysisTemplate + listKind: AnalysisTemplateList + plural: analysistemplates + shortNames: + - at + singular: analysistemplate + preserveUnknownFields: false + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Time since resource was created + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + metrics: + items: + properties: + consecutiveErrorLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + consecutiveSuccessLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + count: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + failureCondition: + type: string + failureLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + inconclusiveLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + initialDelay: + type: string + interval: + type: string + name: + type: string + provider: + properties: + cloudWatch: + properties: + interval: + type: string + metricDataQueries: + items: + properties: + expression: + type: string + id: + type: string + label: + type: string + metricStat: + properties: + metric: + properties: + dimensions: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + metricName: + type: string + namespace: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + stat: + type: string + unit: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + returnData: + type: boolean + type: object + type: array + required: + - metricDataQueries + type: object + datadog: + properties: + aggregator: + enum: + - avg + - min + - max + - sum + - last + - percentile + - mean + - l2norm + - area + type: string + apiVersion: + default: v1 + enum: + - v1 + - v2 + type: string + formula: + type: string + interval: + default: 5m + type: string + queries: + additionalProperties: + type: string + type: object + query: + type: string + secretRef: + properties: + name: + type: string + namespaced: + type: boolean + type: object + type: object + graphite: + properties: + address: + type: string + query: + type: string + type: object + influxdb: + properties: + profile: + type: string + query: + type: string + type: object + job: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + suspend: + type: boolean + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + items: + properties: + name: + type: string + source: + properties: + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + x-kubernetes-preserve-unknown-fields: true + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + required: + - spec + type: object + kayenta: + properties: + address: + type: string + application: + type: string + canaryConfigName: + type: string + configurationAccountName: + type: string + metricsAccountName: + type: string + scopes: + items: + properties: + controlScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + experimentScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + name: + type: string + required: + - controlScope + - experimentScope + - name + type: object + type: array + storageAccountName: + type: string + threshold: + properties: + marginal: + format: int64 + type: integer + pass: + format: int64 + type: integer + required: + - marginal + - pass + type: object + required: + - address + - application + - canaryConfigName + - configurationAccountName + - metricsAccountName + - scopes + - storageAccountName + - threshold + type: object + newRelic: + properties: + profile: + type: string + query: + type: string + timeout: + format: int64 + type: integer + required: + - query + type: object + plugin: + type: object + x-kubernetes-preserve-unknown-fields: true + prometheus: + properties: + address: + type: string + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + query: + type: string + rangeQuery: + properties: + end: + type: string + start: + type: string + step: + type: string + type: object + timeout: + format: int64 + type: integer + type: object + skywalking: + properties: + address: + type: string + interval: + type: string + query: + type: string + type: object + wavefront: + properties: + address: + type: string + query: + type: string + type: object + web: + properties: + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + body: + type: string + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + jsonBody: + type: object + x-kubernetes-preserve-unknown-fields: true + jsonPath: + type: string + method: + type: string + timeoutSeconds: + format: int64 + type: integer + url: + type: string + required: + - url + type: object + type: object + successCondition: + type: string + required: + - name + - provider + type: object + type: array + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: clusteranalysistemplates.argoproj.io +spec: + group: argoproj.io + names: + kind: ClusterAnalysisTemplate + listKind: ClusterAnalysisTemplateList + plural: clusteranalysistemplates + shortNames: + - cat + singular: clusteranalysistemplate + preserveUnknownFields: false + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Time since resource was created + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + metrics: + items: + properties: + consecutiveErrorLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + consecutiveSuccessLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + count: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + failureCondition: + type: string + failureLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + inconclusiveLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + initialDelay: + type: string + interval: + type: string + name: + type: string + provider: + properties: + cloudWatch: + properties: + interval: + type: string + metricDataQueries: + items: + properties: + expression: + type: string + id: + type: string + label: + type: string + metricStat: + properties: + metric: + properties: + dimensions: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + metricName: + type: string + namespace: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + stat: + type: string + unit: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + returnData: + type: boolean + type: object + type: array + required: + - metricDataQueries + type: object + datadog: + properties: + aggregator: + enum: + - avg + - min + - max + - sum + - last + - percentile + - mean + - l2norm + - area + type: string + apiVersion: + default: v1 + enum: + - v1 + - v2 + type: string + formula: + type: string + interval: + default: 5m + type: string + queries: + additionalProperties: + type: string + type: object + query: + type: string + secretRef: + properties: + name: + type: string + namespaced: + type: boolean + type: object + type: object + graphite: + properties: + address: + type: string + query: + type: string + type: object + influxdb: + properties: + profile: + type: string + query: + type: string + type: object + job: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + suspend: + type: boolean + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + items: + properties: + name: + type: string + source: + properties: + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + x-kubernetes-preserve-unknown-fields: true + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + required: + - spec + type: object + kayenta: + properties: + address: + type: string + application: + type: string + canaryConfigName: + type: string + configurationAccountName: + type: string + metricsAccountName: + type: string + scopes: + items: + properties: + controlScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + experimentScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + name: + type: string + required: + - controlScope + - experimentScope + - name + type: object + type: array + storageAccountName: + type: string + threshold: + properties: + marginal: + format: int64 + type: integer + pass: + format: int64 + type: integer + required: + - marginal + - pass + type: object + required: + - address + - application + - canaryConfigName + - configurationAccountName + - metricsAccountName + - scopes + - storageAccountName + - threshold + type: object + newRelic: + properties: + profile: + type: string + query: + type: string + timeout: + format: int64 + type: integer + required: + - query + type: object + plugin: + type: object + x-kubernetes-preserve-unknown-fields: true + prometheus: + properties: + address: + type: string + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + query: + type: string + rangeQuery: + properties: + end: + type: string + start: + type: string + step: + type: string + type: object + timeout: + format: int64 + type: integer + type: object + skywalking: + properties: + address: + type: string + interval: + type: string + query: + type: string + type: object + wavefront: + properties: + address: + type: string + query: + type: string + type: object + web: + properties: + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + body: + type: string + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + jsonBody: + type: object + x-kubernetes-preserve-unknown-fields: true + jsonPath: + type: string + method: + type: string + timeoutSeconds: + format: int64 + type: integer + url: + type: string + required: + - url + type: object + type: object + successCondition: + type: string + required: + - name + - provider + type: object + type: array + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: experiments.argoproj.io +spec: + group: argoproj.io + names: + kind: Experiment + listKind: ExperimentList + plural: experiments + shortNames: + - exp + singular: experiment + preserveUnknownFields: false + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Experiment status + jsonPath: .status.phase + name: Status + type: string + - description: Time since resource was created + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + analyses: + items: + properties: + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + required: + - name + type: object + type: array + clusterScope: + type: boolean + name: + type: string + requiredForCompletion: + type: boolean + templateName: + type: string + required: + - name + - templateName + type: object + type: array + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + duration: + type: string + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + progressDeadlineSeconds: + format: int32 + type: integer + scaleDownDelaySeconds: + format: int32 + type: integer + templates: + items: + properties: + minReadySeconds: + format: int32 + type: integer + name: + type: string + replicas: + format: int32 + type: integer + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + service: + properties: + name: + type: string + type: object + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + items: + properties: + name: + type: string + source: + properties: + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + x-kubernetes-preserve-unknown-fields: true + required: + - containers + type: object + type: object + required: + - name + - selector + - template + type: object + type: array + terminate: + type: boolean + required: + - templates + type: object + status: + properties: + analysisRuns: + items: + properties: + analysisRun: + type: string + message: + type: string + name: + type: string + phase: + type: string + required: + - analysisRun + - name + - phase + type: object + type: array + availableAt: + format: date-time + type: string + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + message: + type: string + phase: + type: string + templateStatuses: + items: + properties: + availableReplicas: + format: int32 + type: integer + collisionCount: + format: int32 + type: integer + lastTransitionTime: + format: date-time + type: string + message: + type: string + name: + type: string + podTemplateHash: + type: string + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + serviceName: + type: string + status: + type: string + updatedReplicas: + format: int32 + type: integer + required: + - availableReplicas + - name + - readyReplicas + - replicas + - updatedReplicas + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: rollouts.argoproj.io +spec: + group: argoproj.io + names: + kind: Rollout + listKind: RolloutList + plural: rollouts + shortNames: + - ro + singular: rollout + preserveUnknownFields: false + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Number of desired pods + jsonPath: .spec.replicas + name: Desired + type: integer + - description: Total number of non-terminated pods targeted by this rollout + jsonPath: .status.replicas + name: Current + type: integer + - description: Total number of non-terminated pods targeted by this rollout that + have the desired template spec + jsonPath: .status.updatedReplicas + name: Up-to-date + type: integer + - description: Total number of available pods (ready for at least minReadySeconds) + targeted by this rollout + jsonPath: .status.availableReplicas + name: Available + type: integer + - description: Time since resource was created + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + analysis: + properties: + successfulRunHistoryLimit: + format: int32 + type: integer + unsuccessfulRunHistoryLimit: + format: int32 + type: integer + type: object + minReadySeconds: + format: int32 + type: integer + paused: + type: boolean + progressDeadlineAbort: + type: boolean + progressDeadlineSeconds: + format: int32 + type: integer + replicas: + format: int32 + type: integer + restartAt: + format: date-time + type: string + revisionHistoryLimit: + format: int32 + type: integer + rollbackWindow: + properties: + revisions: + format: int32 + type: integer + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + strategy: + properties: + blueGreen: + properties: + abortScaleDownDelaySeconds: + format: int32 + type: integer + activeMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + activeService: + type: string + antiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + properties: + weight: + format: int32 + type: integer + required: + - weight + type: object + requiredDuringSchedulingIgnoredDuringExecution: + type: object + type: object + autoPromotionEnabled: + type: boolean + autoPromotionSeconds: + format: int32 + type: integer + maxUnavailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + postPromotionAnalysis: + properties: + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + podTemplateHashValue: + type: string + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + prePromotionAnalysis: + properties: + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + podTemplateHashValue: + type: string + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + previewMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + previewReplicaCount: + format: int32 + type: integer + previewService: + type: string + scaleDownDelayRevisionLimit: + format: int32 + type: integer + scaleDownDelaySeconds: + format: int32 + type: integer + required: + - activeService + type: object + canary: + properties: + abortScaleDownDelaySeconds: + format: int32 + type: integer + analysis: + properties: + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + podTemplateHashValue: + type: string + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + startingStep: + format: int32 + type: integer + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + antiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + properties: + weight: + format: int32 + type: integer + required: + - weight + type: object + requiredDuringSchedulingIgnoredDuringExecution: + type: object + type: object + canaryMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + canaryService: + type: string + dynamicStableScale: + type: boolean + maxSurge: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + minPodsPerReplicaSet: + format: int32 + type: integer + pingPong: + properties: + pingService: + type: string + pongService: + type: string + required: + - pingService + - pongService + type: object + scaleDownDelayRevisionLimit: + format: int32 + type: integer + scaleDownDelaySeconds: + format: int32 + type: integer + stableMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + stableService: + type: string + steps: + items: + properties: + analysis: + properties: + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + podTemplateHashValue: + type: string + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + experiment: + properties: + analyses: + items: + properties: + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + podTemplateHashValue: + type: string + type: object + required: + - name + type: object + type: array + clusterScope: + type: boolean + name: + type: string + requiredForCompletion: + type: boolean + templateName: + type: string + required: + - name + - templateName + type: object + type: array + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + duration: + type: string + templates: + items: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + name: + type: string + replicas: + format: int32 + type: integer + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + service: + properties: + name: + type: string + type: object + specRef: + type: string + weight: + format: int32 + type: integer + required: + - name + - specRef + type: object + type: array + required: + - templates + type: object + pause: + properties: + duration: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + plugin: + properties: + config: + type: object + x-kubernetes-preserve-unknown-fields: true + name: + type: string + required: + - name + type: object + setCanaryScale: + properties: + matchTrafficWeight: + type: boolean + replicas: + format: int32 + type: integer + weight: + format: int32 + type: integer + type: object + setHeaderRoute: + properties: + match: + items: + properties: + headerName: + type: string + headerValue: + properties: + exact: + type: string + prefix: + type: string + regex: + type: string + type: object + required: + - headerName + - headerValue + type: object + type: array + name: + type: string + type: object + setMirrorRoute: + properties: + match: + items: + properties: + headers: + additionalProperties: + properties: + exact: + type: string + prefix: + type: string + regex: + type: string + type: object + type: object + method: + properties: + exact: + type: string + prefix: + type: string + regex: + type: string + type: object + path: + properties: + exact: + type: string + prefix: + type: string + regex: + type: string + type: object + type: object + type: array + name: + type: string + percentage: + format: int32 + type: integer + required: + - name + type: object + setWeight: + format: int32 + type: integer + type: object + type: array + trafficRouting: + properties: + alb: + properties: + annotationPrefix: + type: string + ingress: + type: string + ingresses: + items: + type: string + type: array + rootService: + type: string + servicePort: + format: int32 + type: integer + stickinessConfig: + properties: + durationSeconds: + format: int64 + type: integer + enabled: + type: boolean + required: + - durationSeconds + - enabled + type: object + required: + - servicePort + type: object + ambassador: + properties: + mappings: + items: + type: string + type: array + required: + - mappings + type: object + apisix: + properties: + route: + properties: + name: + type: string + rules: + items: + type: string + type: array + required: + - name + type: object + type: object + appMesh: + properties: + virtualNodeGroup: + properties: + canaryVirtualNodeRef: + properties: + name: + type: string + required: + - name + type: object + stableVirtualNodeRef: + properties: + name: + type: string + required: + - name + type: object + required: + - canaryVirtualNodeRef + - stableVirtualNodeRef + type: object + virtualService: + properties: + name: + type: string + routes: + items: + type: string + type: array + required: + - name + type: object + type: object + istio: + properties: + destinationRule: + properties: + canarySubsetName: + type: string + name: + type: string + stableSubsetName: + type: string + required: + - canarySubsetName + - name + - stableSubsetName + type: object + virtualService: + properties: + name: + type: string + routes: + items: + type: string + type: array + tcpRoutes: + items: + properties: + port: + format: int64 + type: integer + type: object + type: array + tlsRoutes: + items: + properties: + port: + format: int64 + type: integer + sniHosts: + items: + type: string + type: array + type: object + type: array + required: + - name + type: object + virtualServices: + items: + properties: + name: + type: string + routes: + items: + type: string + type: array + tcpRoutes: + items: + properties: + port: + format: int64 + type: integer + type: object + type: array + tlsRoutes: + items: + properties: + port: + format: int64 + type: integer + sniHosts: + items: + type: string + type: array + type: object + type: array + required: + - name + type: object + type: array + type: object + managedRoutes: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + maxTrafficWeight: + format: int32 + type: integer + nginx: + properties: + additionalIngressAnnotations: + additionalProperties: + type: string + type: object + annotationPrefix: + type: string + canaryIngressAnnotations: + additionalProperties: + type: string + type: object + stableIngress: + type: string + stableIngresses: + items: + type: string + type: array + type: object + plugins: + type: object + x-kubernetes-preserve-unknown-fields: true + smi: + properties: + rootService: + type: string + trafficSplitName: + type: string + type: object + traefik: + properties: + weightedTraefikServiceName: + type: string + required: + - weightedTraefikServiceName + type: object + type: object + type: object + type: object + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + items: + properties: + name: + type: string + source: + properties: + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + x-kubernetes-preserve-unknown-fields: true + type: array + required: + - containers + type: object + type: object + workloadRef: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + scaleDown: + type: string + type: object + type: object + status: + properties: + HPAReplicas: + format: int32 + type: integer + abort: + type: boolean + abortedAt: + format: date-time + type: string + alb: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + albs: + items: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + type: array + availableReplicas: + format: int32 + type: integer + blueGreen: + properties: + activeSelector: + type: string + postPromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + prePromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + previewSelector: + type: string + scaleUpPreviewCheckPoint: + type: boolean + type: object + canary: + properties: + currentBackgroundAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + currentExperiment: + type: string + currentStepAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + stablePingPong: + type: string + stepPluginStatuses: + items: + properties: + backoff: + type: string + disabled: + type: boolean + executions: + format: int32 + type: integer + finishedAt: + format: date-time + type: string + index: + format: int32 + type: integer + message: + type: string + name: + type: string + operation: + type: string + phase: + type: string + startedAt: + format: date-time + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + updatedAt: + format: date-time + type: string + required: + - index + - name + - operation + type: object + type: array + weights: + properties: + additional: + items: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + type: array + canary: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + stable: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + verified: + type: boolean + required: + - canary + - stable + type: object + type: object + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + controllerPause: + type: boolean + currentPodHash: + type: string + currentStepHash: + type: string + currentStepIndex: + format: int32 + type: integer + message: + type: string + observedGeneration: + type: string + pauseConditions: + items: + properties: + reason: + type: string + startTime: + format: date-time + type: string + required: + - reason + - startTime + type: object + type: array + phase: + type: string + promoteFull: + type: boolean + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + restartedAt: + format: date-time + type: string + selector: + type: string + stableRS: + type: string + updatedReplicas: + format: int32 + type: integer + workloadObservedGeneration: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.HPAReplicas + status: {} diff --git a/scripts/kind/kind-config.yaml b/scripts/kind/kind-config.yaml new file mode 100644 index 00000000..8afdaab3 --- /dev/null +++ b/scripts/kind/kind-config.yaml @@ -0,0 +1,39 @@ +######################################################################## +# https://kind.sigs.k8s.io/docs/user/configuration/ +######################################################################## +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +name: kagent + +# network configuration +networking: + # WARNING: It is _strongly_ recommended that you keep this the default + # (127.0.0.1) for security reasons. However, it is possible to change this. + apiServerAddress: "127.0.0.1" + # By default, the API server listens on a random open port. + # You may choose a specific port but probably don't need to in most cases. + # Using a random port makes it easier to spin up multiple clusters. + # apiServerPort: 6443 + +# this may be used to e.g. disable beta / alpha APIs. +runtimeConfig: + "api/alpha": "false" + +# add to the apiServer certSANs the name of the docker (dind) service in order to be able to reach the cluster through it +kubeadmConfigPatchesJSON6902: + - group: kubeadm.k8s.io + version: v1beta2 + kind: ClusterConfiguration + patch: | + - op: add + path: /apiServer/certSANs/- + value: docker + +# this is the default configuration for nodes +nodes: + - role: control-plane + extraPortMappings: + - containerPort: 30884 + hostPort: 30884 + - containerPort: 30885 + hostPort: 30885 \ No newline at end of file diff --git a/scripts/kind/test-values-e2e.yaml b/scripts/kind/test-values-e2e.yaml new file mode 100644 index 00000000..9460dde2 --- /dev/null +++ b/scripts/kind/test-values-e2e.yaml @@ -0,0 +1,18 @@ +service: + type: NodePort + ports: + tools: + nodePort: 30885 + +tools: + image: + registry: cr.kagent.dev + +otel: + tracing: + enabled: true + exporter: + otlp: + endpoint: http://host.docker.internal:4317 + timeout: 15 + insecure: true \ No newline at end of file diff --git a/scripts/kind/test-values.yaml b/scripts/kind/test-values.yaml new file mode 100644 index 00000000..f06a7cc0 --- /dev/null +++ b/scripts/kind/test-values.yaml @@ -0,0 +1,18 @@ +service: + type: NodePort + ports: + tools: + nodePort: 30884 + +tools: + image: + registry: cr.kagent.dev + +otel: + tracing: + enabled: true + exporter: + otlp: + endpoint: http://host.docker.internal:4317 + timeout: 15 + insecure: true \ No newline at end of file diff --git a/test/e2e/cli_test.go b/test/e2e/cli_test.go new file mode 100644 index 00000000..61285b06 --- /dev/null +++ b/test/e2e/cli_test.go @@ -0,0 +1,633 @@ +package e2e + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Test suite setup +var _ = Describe("KAgent Tools E2E Tests", func() { + var ( + ctx context.Context + cancel context.CancelFunc + ) + + BeforeEach(func() { + ctx, cancel = context.WithTimeout(context.Background(), 60*time.Second) + + // Set OTEL environment variables for testing + os.Setenv("OTEL_SERVICE_NAME", "kagent-tools-e2e-test") + os.Setenv("LOG_LEVEL", "debug") + }) + + AfterEach(func() { + if cancel != nil { + cancel() + } + os.Unsetenv("OTEL_SERVICE_NAME") + os.Unsetenv("LOG_LEVEL") + }) + + Describe("HTTP Server Tests", func() { + It("should start and stop HTTP server successfully", func() { + config := TestServerConfig{ + Port: 8085, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Test health endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + Expect(err).NotTo(HaveOccurred(), "Health endpoint should be accessible") + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + resp.Body.Close() + + // Check server output + output := server.GetOutput() + Expect(output).To(ContainSubstring("Running KAgent Tools Server")) + Expect(output).To(ContainSubstring(fmt.Sprintf(":%d", config.Port))) + + // Stop server + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + + // Wait for server to fully shutdown + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() + + err = server.waitForShutdown(shutdownCtx, config.Port) + Expect(err).NotTo(HaveOccurred(), "Server should shut down completely") + }) + + It("should start server with specific tools", func() { + config := TestServerConfig{ + Port: 8086, + Tools: []string{"utils", "k8s"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for tool registration + output := server.GetOutput() + Expect(output).To(ContainSubstring("RegisterTools initialized")) + Expect(output).To(ContainSubstring("utils")) + Expect(output).To(ContainSubstring("k8s")) + + // Stop server + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + + It("should start server with all tools enabled", func() { + config := TestServerConfig{ + Port: 8087, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for all tools registration + output := server.GetOutput() + Expect(output).To(ContainSubstring("RegisterTools initialized")) + Expect(output).To(ContainSubstring("Running KAgent Tools Server")) + + // Stop server + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + + It("should start server with kubeconfig parameter", func() { + // Create a temporary kubeconfig file + tempDir := GinkgoT().TempDir() + kubeconfigPath := filepath.Join(tempDir, "kubeconfig") + + kubeconfigContent := `apiVersion: v1 +kind: Config +clusters: +- cluster: + server: https://test-cluster + name: test-cluster +contexts: +- context: + cluster: test-cluster + user: test-user + name: test-context +current-context: test-context +users: +- name: test-user + user: + token: test-token +` + + err := os.WriteFile(kubeconfigPath, []byte(kubeconfigContent), 0644) + Expect(err).NotTo(HaveOccurred(), "Should create temporary kubeconfig file") + + config := TestServerConfig{ + Port: 8088, + Kubeconfig: kubeconfigPath, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err = server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for kubeconfig setting + output := server.GetOutput() + Expect(output).To(ContainSubstring("RegisterTools initialized")) + Expect(output).To(ContainSubstring("Running KAgent Tools Server")) + + // Stop server + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + + It("should handle invalid tool names gracefully", func() { + config := TestServerConfig{ + Port: 18190, + Tools: []string{"invalid-tool", "utils"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start even with invalid tools") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for error about invalid tool + output := server.GetOutput() + Expect(output).To(ContainSubstring("Unknown tool specified")) + Expect(output).To(ContainSubstring("invalid-tool")) + + // Valid tools should still be registered + Expect(output).To(ContainSubstring("RegisterTools initialized")) + Expect(output).To(ContainSubstring("utils")) + + // Stop server + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + + It("should handle graceful shutdown", func() { + config := TestServerConfig{ + Port: 8100, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Test health endpoint to ensure server is fully ready + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + Expect(err).NotTo(HaveOccurred(), "Health endpoint should be accessible") + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + resp.Body.Close() + + // Stop server and measure shutdown time + start := time.Now() + err = server.Stop() + duration := time.Since(start) + + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + Expect(duration).To(BeNumerically("<", 10*time.Second), "Shutdown should complete within reasonable time") + + // Check server output for graceful shutdown + output := server.GetOutput() + Expect(output).To(ContainSubstring("Running KAgent Tools Server")) + + // Wait for server to fully shutdown + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() + + err = server.waitForShutdown(shutdownCtx, config.Port) + Expect(err).NotTo(HaveOccurred(), "Server should shut down completely") + }) + + It("should handle concurrent requests", func() { + config := TestServerConfig{ + Port: 8088, + Tools: []string{"utils", "k8s"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Create multiple concurrent requests + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + Expect(err).NotTo(HaveOccurred(), "Concurrent request %d should succeed", id) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + resp.Body.Close() + }(i) + } + + wg.Wait() + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + + It("should expose metrics endpoint", func() { + config := TestServerConfig{ + Port: 18190, + Tools: []string{"utils"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Test metrics endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/metrics", config.Port)) + Expect(err).NotTo(HaveOccurred(), "Metrics endpoint should be accessible") + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + + // Read and verify metrics content + body, err := io.ReadAll(resp.Body) + Expect(err).NotTo(HaveOccurred()) + resp.Body.Close() + + metricsContent := string(body) + Expect(metricsContent).To(ContainSubstring("go_")) + Expect(metricsContent).To(ContainSubstring("process_")) + + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + }) + + Describe("STDIO Server Tests", func() { + It("should start STDIO server successfully", func() { + config := TestServerConfig{ + Stdio: true, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for STDIO mode + output := server.GetOutput() + Expect(output).To(ContainSubstring("Running KAgent Tools Server STDIO")) + + // Stop server + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + }) + + Describe("Tool Registration Tests", func() { + It("should register single tool correctly", func() { + config := TestServerConfig{ + Port: 8087, + Tools: []string{"k8s"}, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Verify registered tools + output := server.GetOutput() + Expect(output).To(ContainSubstring("Running KAgent Tools Server")) + Expect(output).To(ContainSubstring("k8s")) + + // Test health endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + Expect(err).NotTo(HaveOccurred(), "Health endpoint should be accessible") + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + resp.Body.Close() + + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + + It("should register multiple tools correctly", func() { + config := TestServerConfig{ + Port: 8088, + Tools: []string{"k8s", "prometheus", "utils"}, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Verify registered tools + output := server.GetOutput() + Expect(output).To(ContainSubstring("Running KAgent Tools Server")) + for _, tool := range []string{"k8s", "prometheus", "utils"} { + Expect(output).To(ContainSubstring(tool)) + } + + // Test health endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + Expect(err).NotTo(HaveOccurred(), "Health endpoint should be accessible") + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + resp.Body.Close() + + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + + It("should register all tools implicitly", func() { + config := TestServerConfig{ + Port: 18190, + Tools: []string{}, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Verify all tools are registered + output := server.GetOutput() + Expect(output).To(ContainSubstring("RegisterTools initialized")) + Expect(output).To(ContainSubstring("Running KAgent Tools Server")) + + // Test health endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + Expect(err).NotTo(HaveOccurred(), "Health endpoint should be accessible") + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + resp.Body.Close() + + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + }) + + Describe("Error Handling Tests", func() { + It("should handle malformed requests gracefully", func() { + config := TestServerConfig{ + Port: 8089, + Tools: []string{"utils"}, + Stdio: false, + Timeout: 10 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Test malformed request + req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/nonexistent", config.Port), strings.NewReader("invalid json")) + Expect(err).NotTo(HaveOccurred()) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusBadRequest)) + resp.Body.Close() + + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + }) + + Describe("Environment and Configuration Tests", func() { + It("should handle environment variables correctly", func() { + // Set environment variables + originalEnv := os.Environ() + defer func() { + os.Clearenv() + for _, env := range originalEnv { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + os.Setenv(parts[0], parts[1]) + } + } + }() + + os.Setenv("LOG_LEVEL", "info") + os.Setenv("OTEL_SERVICE_NAME", "test-kagent-tools") + + config := TestServerConfig{ + Port: 18195, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + + // Start server + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output + output := server.GetOutput() + Expect(output).To(ContainSubstring("Starting kagent-tools-server")) + + // Stop server + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + + It("should validate server binary exists and is executable", func() { + // Check if server binary exists + binaryName := getBinaryName() + binaryPath := fmt.Sprintf("../../bin/%s", binaryName) + _, err := os.Stat(binaryPath) + if os.IsNotExist(err) { + Skip("Server binary not found, skipping test. Run 'make build' first.") + } + Expect(err).NotTo(HaveOccurred(), "Server binary should exist") + + // Test --help flag + helpCtx, helpCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer helpCancel() + + cmd := exec.CommandContext(helpCtx, binaryPath, "--help") + output, err := cmd.CombinedOutput() + Expect(err).NotTo(HaveOccurred(), "Server should respond to --help flag") + + outputStr := string(output) + Expect(outputStr).To(ContainSubstring("KAgent tool server")) + Expect(outputStr).To(ContainSubstring("--port")) + Expect(outputStr).To(ContainSubstring("--stdio")) + Expect(outputStr).To(ContainSubstring("--tools")) + Expect(outputStr).To(ContainSubstring("--kubeconfig")) + }) + }) + + Describe("Concurrent Server Instances", func() { + It("should run multiple server instances concurrently", func() { + var wg sync.WaitGroup + numServers := 3 + servers := make([]*TestServer, numServers) + + // Start multiple servers on different ports + for i := 0; i < numServers; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + + config := TestServerConfig{ + Port: 18092 + index, + Tools: []string{"utils"}, + Stdio: false, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + servers[index] = server + + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server %d should start successfully", index) + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Test health endpoint + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + Expect(err).NotTo(HaveOccurred(), "Health endpoint should be accessible for server %d", index) + if resp != nil { + resp.Body.Close() + } + }(i) + } + + wg.Wait() + + // Stop all servers + for i, server := range servers { + if server != nil { + err := server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server %d should stop gracefully", i) + } + } + }) + }) + + Describe("Telemetry Tests", func() { + It("should initialize telemetry correctly", func() { + config := TestServerConfig{ + Port: 18092, + Tools: []string{"utils"}, + Timeout: 30 * time.Second, + } + + server := NewTestServer(config) + err := server.Start(ctx, config) + Expect(err).NotTo(HaveOccurred(), "Server should start successfully") + + // Wait for server to be ready + time.Sleep(3 * time.Second) + + // Check server output for telemetry initialization + output := server.GetOutput() + Expect(output).To(ContainSubstring("Starting kagent-tools-server")) + + // Make a request to generate telemetry + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/health", config.Port)) + Expect(err).NotTo(HaveOccurred(), "Health endpoint should be accessible") + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + resp.Body.Close() + + // Check server output for successful startup + output = server.GetOutput() + Expect(output).To(ContainSubstring("Running KAgent Tools Server")) + + err = server.Stop() + Expect(err).NotTo(HaveOccurred(), "Server should stop gracefully") + }) + }) +}) + +// Helper functions for test setup +func init() { + // Ensure the binary exists before running tests + binaryName := getBinaryName() + binaryPath := fmt.Sprintf("../../bin/%s", binaryName) + if _, err := os.Stat(binaryPath); os.IsNotExist(err) { + // Try to build the binary + cmd := exec.Command("make", "build") + cmd.Dir = "../.." + if err := cmd.Run(); err != nil { + panic(fmt.Sprintf("Failed to build server binary: %v", err)) + } + } +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go new file mode 100644 index 00000000..1f957924 --- /dev/null +++ b/test/e2e/e2e_test.go @@ -0,0 +1,13 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "testing" +) + +// TestE2EK8s is the main test runner for Kubernetes E2E tests +func TestE2EK8s(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Tools E2E Suite") +} diff --git a/test/e2e/helpers_test.go b/test/e2e/helpers_test.go new file mode 100644 index 00000000..59008d85 --- /dev/null +++ b/test/e2e/helpers_test.go @@ -0,0 +1,606 @@ +package e2e + +import ( + "bufio" + "context" + "fmt" + "io" + "log/slog" + "net/http" + "os" + "os/exec" + "runtime" + "strings" + "sync" + "time" + + "github.com/kagent-dev/tools/internal/commands" + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/client/transport" + "github.com/mark3labs/mcp-go/mcp" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// getBinaryName returns the platform-specific binary name +func getBinaryName() string { + osName := runtime.GOOS + archName := runtime.GOARCH + return fmt.Sprintf("kagent-tools-%s-%s", osName, archName) +} + +// TestServerConfig holds configuration for server tests +type TestServerConfig struct { + Port int + Tools []string + Kubeconfig string + Stdio bool + Timeout time.Duration +} + +// TestServer represents a test server instance +type TestServer struct { + cmd *exec.Cmd + port int + stdio bool + cancel context.CancelFunc + done chan struct{} + output strings.Builder + mu sync.RWMutex +} + +// NewTestServer creates a new test server instance +func NewTestServer(config TestServerConfig) *TestServer { + return &TestServer{ + port: config.Port, + stdio: config.Stdio, + done: make(chan struct{}), + } +} + +// Start starts the test server +func (ts *TestServer) Start(ctx context.Context, config TestServerConfig) error { + ts.mu.Lock() + defer ts.mu.Unlock() + + // Build command arguments + args := []string{} + if config.Stdio { + args = append(args, "--stdio") + } else { + args = append(args, "--port", fmt.Sprintf("%d", config.Port)) + } + + if len(config.Tools) > 0 { + args = append(args, "--tools", strings.Join(config.Tools, ",")) + } + + if config.Kubeconfig != "" { + args = append(args, "--kubeconfig", config.Kubeconfig) + } + + // Create context with cancellation + ctx, cancel := context.WithCancel(ctx) + ts.cancel = cancel + + // Start server process + binaryName := getBinaryName() + ts.cmd = exec.CommandContext(ctx, fmt.Sprintf("../../bin/%s", binaryName), args...) + ts.cmd.Env = append(os.Environ(), "LOG_LEVEL=debug") + + // Set up output capture + stdout, err := ts.cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("failed to create stdout pipe: %w", err) + } + + stderr, err := ts.cmd.StderrPipe() + if err != nil { + return fmt.Errorf("failed to create stderr pipe: %w", err) + } + + // Start the command + if err := ts.cmd.Start(); err != nil { + return fmt.Errorf("failed to start server: %w", err) + } + + // Start goroutines to capture output + go ts.captureOutput(stdout, "STDOUT") + go ts.captureOutput(stderr, "STDERR") + + // Wait for server to start + if !config.Stdio { + return ts.waitForHTTPServer(ctx, config.Timeout) + } + + return nil +} + +// Stop stops the test server +func (ts *TestServer) Stop() error { + ts.mu.Lock() + defer ts.mu.Unlock() + + if ts.cancel != nil { + ts.cancel() + } + + if ts.cmd != nil && ts.cmd.Process != nil { + // Send interrupt signal for graceful shutdown + if err := ts.cmd.Process.Signal(os.Interrupt); err != nil { + // If interrupt fails, kill the process + _ = ts.cmd.Process.Kill() + } + + // Wait for process to exit with timeout + done := make(chan error, 1) + go func() { + done <- ts.cmd.Wait() + }() + + select { + case <-done: + // Process exited + case <-time.After(8 * time.Second): // Increased timeout + // Timeout, force kill + _ = ts.cmd.Process.Kill() + // Wait a bit more for force kill to complete + select { + case <-done: + case <-time.After(2 * time.Second): + // Force kill timeout, continue anyway + } + } + } + + // Signal done and wait for goroutines to exit + if ts.done != nil { + close(ts.done) + } + + // Give goroutines time to exit + time.Sleep(100 * time.Millisecond) + + return nil +} + +// MCPClient represents a client for communicating with the MCP server using the official mcp-go client +type MCPClient struct { + client *client.Client + log *slog.Logger +} + +// InstallKAgentTools installs KAgent Tools using helm in the specified namespace +func InstallKAgentTools(namespace string, releaseName string) { + // Use longer timeout for helm installation as it can take time to pull images + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + log := slog.Default() + By("Installing KAgent Tools in namespace " + namespace) + log.Info("Installing KAgent Tools", "namespace", namespace) + + // First, try to uninstall any existing release to clean up + log.Info("Cleaning up any existing release", "release", releaseName, "namespace", namespace) + _, _ = commands.NewCommandBuilder("helm"). + WithArgs("uninstall", releaseName). + WithArgs("--namespace", namespace). + WithArgs("--ignore-not-found"). + WithCache(false). + Execute(ctx) + + // install crd scripts/kind/crd-argo.yaml + By("Installing CRDs for KAgent Tools") + _, err := commands.NewCommandBuilder("kubectl"). + WithArgs("apply", "-f", "../../scripts/kind/crd-argo.yaml"). + WithArgs("--namespace", namespace). + WithCache(false). // Don't cache CRD installation + Execute(ctx) + Expect(err).ToNot(HaveOccurred(), "Failed to install CRDs: %v", err) + + // Install KAgent Tools using helm with unique release name + // Use absolute path from project root + output, err := commands.NewCommandBuilder("helm"). + WithArgs("install", releaseName, "../../helm/kagent-tools"). + WithArgs("--namespace", namespace). + WithArgs("-f"). + WithArgs("../../scripts/kind/test-values-e2e.yaml"). + WithArgs("--create-namespace"). + WithArgs("--debug"). + WithArgs("--wait"). + WithArgs("--timeout=1m"). + WithCache(false). // Don't cache helm installation + Execute(ctx) + + Expect(err).ToNot(HaveOccurred(), "Failed to install KAgent Tools: %v %v", err, output) + log.Info("KAgent Tools installation completed", "namespace", namespace, "output", output) + + // Verify the installation by checking if pods are running + By("Verifying KAgent Tools pods are running") + log.Info("Verifying KAgent Tools pods", "namespace", namespace) + + Eventually(func() bool { + ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancel() + + output, err := commands.NewCommandBuilder("kubectl"). + WithArgs("get", "pods", "-n", namespace, "-l", "app.kubernetes.io/name=kagent-tools", "-o", "jsonpath={.items[*].status.phase}"). + Execute(ctx) + + if err != nil { + log.Error("Failed to get pod status", "error", err) + return false + } + + log.Info("Pod status check", "namespace", namespace, "output", output) + // Check if all pods are in Running state + return output == "Running" || (len(output) > 0 && !contains(output, "Pending") && !contains(output, "Failed")) + }, 60*time.Second, 5*time.Second).Should(BeTrue(), "KAgent Tools pods should be running") + + log.Info("KAgent Tools pods are running", "namespace", namespace) + //validate service nodePort == 30885 + By("Validating KAgent Tools service is accessible") + nodePort, err := commands.NewCommandBuilder("kubectl"). + WithArgs("get", "svc", "-n", namespace, "-o", "jsonpath={.items[0].spec.ports[0].nodePort}"). + Execute(ctx) + Expect(err).ToNot(HaveOccurred(), "Failed to get service nodePort: %v", err) + Expect(nodePort).To(Equal("30885")) +} + +// GetMCPClient creates a new MCP client configured for the e2e test environment using the official mcp-go client +func GetMCPClient() (*MCPClient, error) { + // Create HTTP transport for the MCP server + httpTransport, err := transport.NewStreamableHTTP("http://127.0.0.1:30885/mcp", transport.WithHTTPTimeout(15*time.Second)) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP transport: %w", err) + } + + // Create the official MCP client + mcpClient := client.NewClient(httpTransport) + + // Start the client + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + if err := mcpClient.Start(ctx); err != nil { + return nil, fmt.Errorf("failed to start MCP client: %w", err) + } + + // Initialize the client + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "e2e-test-client", + Version: "1.0.0", + } + initRequest.Params.Capabilities = mcp.ClientCapabilities{} + + _, err = mcpClient.Initialize(ctx, initRequest) + if err != nil { + return nil, fmt.Errorf("failed to initialize MCP client: %w", err) + } + + mcpHelper := &MCPClient{ + client: mcpClient, + log: slog.Default(), + } + + // Validate connection by listing tools + tools, err := mcpHelper.listTools() + if len(tools) == 0 { + return nil, fmt.Errorf("no tools found in MCP server: %w", err) + } + slog.Default().Info("MCP Client created", "baseURL", "http://127.0.0.1:30885/mcp", "tools", len(tools)) + return mcpHelper, err +} + +// listTools calls the tools/list method to get available tools +func (c *MCPClient) listTools() ([]interface{}, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + request := mcp.ListToolsRequest{} + result, err := c.client.ListTools(ctx, request) + if err != nil { + return nil, err + } + + // Convert tools to interface{} slice for compatibility + tools := make([]interface{}, len(result.Tools)) + for i, tool := range result.Tools { + tools[i] = tool + } + + return tools, nil +} + +// k8sListResources calls the k8s_get_resources tool +func (c *MCPClient) k8sListResources(resourceType string) (interface{}, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + type K8sArgs struct { + ResourceType string `json:"resource_type"` + Output string `json:"output"` + } + + arguments := K8sArgs{ + ResourceType: resourceType, + Output: "json", + } + + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "k8s_get_resources", + Arguments: arguments, + }, + } + + result, err := c.client.CallTool(ctx, request) + if err != nil { + return nil, err + } + if result.IsError { + return nil, fmt.Errorf("tool call failed: %s", result.Content) + } + return result, nil +} + +// helmListReleases calls the helm_list_releases tool +func (c *MCPClient) helmListReleases() (interface{}, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + type HelmArgs struct { + AllNamespaces string `json:"all_namespaces"` + Output string `json:"output"` + } + + arguments := HelmArgs{ + AllNamespaces: "true", + Output: "json", + } + + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "helm_list_releases", + Arguments: arguments, + }, + } + + result, err := c.client.CallTool(ctx, request) + if err != nil { + return nil, err + } + if result.IsError { + return nil, fmt.Errorf("tool call failed: %s", result.Content) + } + return result, nil +} + +// istioInstall calls the istio_install_istio tool +func (c *MCPClient) istioInstall(profile string) (interface{}, error) { + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) // Istio install can take time + defer cancel() + + type IstioArgs struct { + Profile string `json:"profile"` + } + + arguments := IstioArgs{ + Profile: profile, + } + + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "istio_install_istio", + Arguments: arguments, + }, + } + + result, err := c.client.CallTool(ctx, request) + if err != nil { + return nil, err + } + if result.IsError { + return nil, fmt.Errorf("tool call failed: %s", result.Content) + } + return result, nil +} + +// argoRolloutsList calls the argo_rollouts_get tool to list rollouts +func (c *MCPClient) argoRolloutsList(namespace string) (interface{}, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + type ArgoArgs struct { + Namespace string `json:"namespace"` + Output string `json:"output"` + } + + arguments := ArgoArgs{ + Namespace: namespace, + Output: "json", + } + + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "argo_rollouts_list", + Arguments: arguments, + }, + } + + result, err := c.client.CallTool(ctx, request) + if err != nil { + return nil, err + } + if result.IsError { + return nil, fmt.Errorf("tool call failed: %s", result.Content) + } + return result, nil +} + +// ciliumStatus calls the cilium_status_and_version tool +func (c *MCPClient) ciliumStatus() (interface{}, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + request := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "cilium_status_and_version", + Arguments: nil, + }, + } + + result, err := c.client.CallTool(ctx, request) + if err != nil { + return nil, err + } + return result, nil +} + +// Constants for default test values +const ( + DefaultReleaseName = "kagent-tools-e2e" + DefaultTestNamespace = "kagent-tools-e2e" + DefaultTimeout = 60 * time.Second // Increased for more realistic timeouts +) + +// CreateNamespace creates a new Kubernetes namespace +func CreateNamespace(namespace string) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + log := slog.Default() + By("Creating namespace " + namespace) + log.Info("Creating namespace", "namespace", namespace) + + // First, check if the namespace already exists + _, err := commands.NewCommandBuilder("kubectl"). + WithArgs("get", "namespace", namespace). + WithCache(false). + Execute(ctx) + + if err == nil { + log.Info("Namespace already exists, skipping creation", "namespace", namespace) + return + } + + // Create the namespace using kubectl + output, err := commands.NewCommandBuilder("kubectl"). + WithArgs("create", "namespace", namespace). + WithCache(false). // Don't cache namespace creation + Execute(ctx) + + // If it's an AlreadyExists error, that's fine - treat it as success + if err != nil && strings.Contains(err.Error(), "AlreadyExists") { + log.Info("Namespace already exists, continuing", "namespace", namespace) + return + } + + Expect(err).ToNot(HaveOccurred(), "Failed to create namespace: %v", err) + log.Info("Namespace creation completed", "namespace", namespace, "output", output) +} + +// DeleteNamespace deletes a Kubernetes namespace +func DeleteNamespace(namespace string) { + // Use longer timeout for namespace deletion as it can take more time + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + log := slog.Default() + By("Deleting namespace " + namespace) + log.Info("Deleting namespace", "namespace", namespace) + + // Delete the namespace using kubectl + output, err := commands.NewCommandBuilder("kubectl"). + WithArgs("delete", "namespace", namespace, "--ignore-not-found=true", "--wait=false"). + WithCache(false). // Don't cache namespace deletion + Execute(ctx) + + Expect(err).ToNot(HaveOccurred(), "Failed to delete namespace: %v", err) + log.Info("Namespace deletion completed", "namespace", namespace, "output", output) +} + +// contains checks if a string contains a substring +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsHelper(s, substr))) +} + +func containsHelper(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +// waitForHTTPServer waits for the HTTP server to become available +func (ts *TestServer) waitForHTTPServer(ctx context.Context, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + url := fmt.Sprintf("http://localhost:%d/health", ts.port) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for server to start") + case <-ticker.C: + resp, err := http.Get(url) + if err == nil { + _ = resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return nil + } + } + } + } +} + +// waitForShutdown waits for the HTTP server to become unavailable +func (ts *TestServer) waitForShutdown(ctx context.Context, port int) error { + url := fmt.Sprintf("http://localhost:%d/health", port) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for server to shutdown") + case <-ticker.C: + _, err := http.Get(url) + if err != nil { + // Server is not accessible, shutdown complete + return nil + } + } + } +} + +// GetOutput returns the captured output +func (ts *TestServer) GetOutput() string { + ts.mu.RLock() + defer ts.mu.RUnlock() + return ts.output.String() +} + +// captureOutput captures output from the server +func (ts *TestServer) captureOutput(reader io.Reader, prefix string) { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + select { + case <-ts.done: + // Shutdown signal received, exit goroutine + return + default: + line := scanner.Text() + ts.mu.Lock() + ts.output.WriteString(fmt.Sprintf("[%s] %s\n", prefix, line)) + ts.mu.Unlock() + } + } +} diff --git a/test/e2e/k8s_test.go b/test/e2e/k8s_test.go new file mode 100644 index 00000000..65ad66ac --- /dev/null +++ b/test/e2e/k8s_test.go @@ -0,0 +1,151 @@ +package e2e + +import ( + "context" + "fmt" + "github.com/kagent-dev/tools/internal/commands" + "github.com/kagent-dev/tools/internal/logger" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +/* +K8s E2E Tests +These tests are used to test the Kubernetes integration of the KAgent Tools. +They are run in a Kubernetes cluster and have working in-cluster resources. + +They test the following: +- KAgent Tools can be installed in a Kubernetes cluster +- KAgent Tools k8s can list all resources in the cluster +- KAgent Tools helm can list all releases in the cluster +- KAgent Tools istioctl can install istio in the cluster +- KAgent Tools cillium can install cillium in the cluster +*/ + +var _ = Describe("KAgent Tools Kubernetes E2E Tests", Ordered, func() { + + var err error + var client *MCPClient + var log = logger.Get() + var namespace = DefaultTestNamespace + var releaseName = DefaultReleaseName + + BeforeAll(func() { + log.Info("Starting KAgent Tools E2E tests") + // Create new namespace + CreateNamespace(namespace) + // Install kagent tools + InstallKAgentTools(namespace, releaseName) + + client, err = GetMCPClient() + Expect(err).ToNot(HaveOccurred(), "Failed to get MCP client: %v", err) + }) + + AfterAll(func() { + log.Info("Cleaning up KAgent Tools E2E tests", "namespace", namespace) + // Delete namespace + if namespace != "" { + DeleteNamespace(namespace) + } + }) + + Describe("KAgent Tools Deployment", func() { + It("should have kagent-tools pods running", func() { + ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancel() + + log.Info("Checking if kagent-tools pods are running", "namespace", namespace) + output, err := commands.NewCommandBuilder("kubectl"). + WithArgs("get", "pods", "-n", namespace, "-l", "app.kubernetes.io/name=kagent-tools", "-o", "json"). + Execute(ctx) + + Expect(err).ToNot(HaveOccurred()) + Expect(output).ToNot(BeEmpty()) + log.Info("Successfully verified kagent-tools pods", "namespace", namespace) + }) + + It("should have kagent-tools service accessible", func() { + ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancel() + + log.Info("Checking if kagent-tools service is accessible", "namespace", namespace) + output, err := commands.NewCommandBuilder("kubectl"). + WithArgs("get", "svc", "-n", namespace, "-l", "app.kubernetes.io/name=kagent-tools", "-o", "json"). + Execute(ctx) + + Expect(err).ToNot(HaveOccurred()) + Expect(output).ToNot(BeEmpty()) + log.Info("Successfully verified kagent-tools service", "namespace", namespace, "output", output) + }) + }) + + Describe("KAgent Tools K8s Operations", func() { + It("should be able to list namespace in the cluster", func() { + log.Info("Testing MCP client connectivity and k8s operations", "namespace", namespace) + + // Test k8s list resources functionality + log.Info("Testing k8s list resources via MCP") + response, err := client.k8sListResources("namespace") + Expect(err).ToNot(HaveOccurred(), "Failed to list k8s resources via MCP: %v", err) + Expect(response).ToNot(BeNil()) + + log.Info("Successfully tested k8s operations via MCP", "namespace", namespace) + }) + }) + + Describe("KAgent Tools Helm Operations", func() { + It("should be able to list all helm releases", func() { + log.Info("Testing helm operations via MCP", "namespace", namespace) + + // Test helm list releases functionality + log.Info("Testing helm list releases via MCP") + response, err := client.helmListReleases() + if err != nil { + log.Info("Helm list releases failed (may be normal)", "error", err) + Skip(fmt.Sprintf("Helm operations not available: %v", err)) + return + } + Expect(response).ToNot(BeNil()) + log.Info("Successfully tested helm operations via MCP", "namespace", namespace) + }) + }) + + Describe("KAgent Tools Istio Operations", func() { + It("should be able to install istio in the cluster", func() { + log.Info("Testing istio operations via MCP", "namespace", namespace) + + // If we get here, MCP is accessible, test istio operations + response, err := client.istioInstall("default") + Expect(err).ToNot(HaveOccurred(), "Failed to install istio via MCP: %v", err) + Expect(response).ToNot(BeNil()) + + log.Info("Successfully tested istio operations via MCP", "namespace", namespace, "response", response) + }) + }) + + Describe("KAgent Tools Cilium Operations", func() { + It("should be able to install cilium in the cluster", func() { + log.Info("Testing cilium operations via MCP", "namespace", namespace) + + // If we get here, MCP is accessible, test cilium operations + response, err := client.ciliumStatus() + Expect(err).ToNot(HaveOccurred(), "Failed to get cilium status via MCP: %v", err) + Expect(response).ToNot(BeNil()) + + log.Info("Successfully tested cilium operations via MCP", "namespace", namespace) + }) + }) + + Describe("KAgent Tools Argo Operations", func() { + It("should be able to list Argo rollouts in the cluster", func() { + log.Info("Testing Argo operations via MCP", "namespace", namespace) + + // If we get here, MCP is accessible, test cilium operations + response, err := client.argoRolloutsList(namespace) + Expect(err).ToNot(HaveOccurred(), "Failed to list argo rollouts via MCP: %v", err) + Expect(response).ToNot(BeNil()) + + log.Info("Successfully tested argo rollouts via MCP", "namespace", namespace) + }) + }) +}) From 8901bc2712c8030562afc7f57439d0318b444545 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Tue, 22 Jul 2025 16:50:23 +0200 Subject: [PATCH 19/41] fix helm-publish Signed-off-by: Dmytro Rashko --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dea88a80..6be3920b 100644 --- a/Makefile +++ b/Makefile @@ -189,7 +189,7 @@ helm-install: helm-version .PHONY: helm-publish helm-publish: helm-version - helm push ./$(HELM_DIST_FOLDER)/kagent-tools-$(VERSION).tgz $(HELM_REPO)/tools/helm + helm push $(HELM_DIST_FOLDER)/kagent-tools-$(VERSION).tgz $(HELM_REPO)/tools/helm .PHONY: create-kind-cluster create-kind-cluster: From 39f44272ec00277472cceb3902125bc7acd7c83a Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Fri, 25 Jul 2025 14:56:49 +0200 Subject: [PATCH 20/41] [FEATURE] Add quickstart guide for agentgateway (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * - 🐛 Fix stdio implementation - 🚀 Add quickstart guide for agentgateway - 📝 Update cursor MCP documentation Signed-off-by: Dmytro Rashko * - 🐛 Fix stdio implementation - 🚀 Add quickstart guide for agentgateway - 📝 Update cursor MCP documentation Signed-off-by: Dmytro Rashko * add homebrew path Signed-off-by: Dmytro Rashko * increase default timeout Signed-off-by: Dmytro Rashko * quickstart updated Signed-off-by: Dmytro Rashko --------- Signed-off-by: Dmytro Rashko --- Makefile | 15 +++++ README.md | 16 +++++- cmd/main.go | 2 +- docs/quickstart.md | 78 ++++++++++++++++++++++++++ helm/kagent-tools/Chart-template.yaml | 3 +- internal/commands/builder.go | 11 +++- internal/commands/builder_test.go | 4 +- internal/logger/logger.go | 24 ++++++-- internal/logger/logger_test.go | 3 +- scripts/agentgateway-config-tools.yaml | 23 ++++++++ 10 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 docs/quickstart.md create mode 100644 scripts/agentgateway-config-tools.yaml diff --git a/Makefile b/Makefile index 6be3920b..8f9fcf97 100644 --- a/Makefile +++ b/Makefile @@ -19,11 +19,13 @@ LDFLAGS := -X github.com/kagent-dev/tools/internal/version.Version=$(VERSION) -X ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin +PATH := $HOME/local/bin:/opt/homebrew/bin/:$(LOCALBIN):$(PATH) HELM_DIST_FOLDER ?= $(shell pwd)/dist .PHONY: clean clean: rm -rf ./bin/kagent-tools-* + rm -rf $(HOME)/.local/bin/kagent-tools-* .PHONY: fmt fmt: ## Run go fmt against code. @@ -210,6 +212,19 @@ otel-local: docker run -d --name jaeger-desktop --restart=always -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/jaeger:2.7.0 open http://localhost:16686/ +.PHONY: tools-install +tools-install: clean + mkdir -p $HOME/.local/bin + go build -ldflags "$(LDFLAGS)" -o $(LOCALBIN)/kagent-tools ./cmd + go build -ldflags "$(LDFLAGS)" -o $(HOME)/.local/bin/kagent-tools ./cmd + $HOME/.local/bin/kagent-tools --version + +.PHONY: run-agentgateway +run-agentgateway: tools-install + open http://localhost:15000/ui + cd scripts \ + && agentgateway -f agentgateway-config-tools.yaml + .PHONY: report/image-cve report/image-cve: docker-build govulncheck echo "Running CVE scan :: CVE -> CSV ... reports/$(SEMVER)/" diff --git a/README.md b/README.md index f26ccf9d..0bc29beb 100644 --- a/README.md +++ b/README.md @@ -28,16 +28,26 @@ This directory contains the Go implementation of all KAgent tools, migrated from - **Bash:** -`curl -sL https://github.com/kagent-dev/tools/blob/main/scripts/install.sh | bash` +```bash +curl -sL https://raw.githubusercontent.com/kagent-dev/tools/refs/heads/main/scripts/install.sh | bash +``` - **Docker:** -`docker run -it --rm ghcr.io/kagent-dev/kagent/tools:` +```bash +docker run -it --rm -p 8084:8084 ghcr.io/kagent-dev/kagent/tools:0.0.10 +``` - **Kubernetes** -`helm upgrade -i kagent-tools --version oci://ghcr.io/kagent-dev/tools/helm/` +```bash +helm upgrade -i -n kagent --create-namespace kagent-tools oci://ghcr.io/kagent-dev/tools/helm/kagent-tools --version 0.0.10 +helm ls -A +``` + +## Quickstart Guide +For a quickstart guide on how to run KAgent tools using AgentGateway, please refer to the [Quickstart Guide](docs/quickstart.md). ## Architecture diff --git a/cmd/main.go b/cmd/main.go index 03a26658..fa737dd6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -89,7 +89,7 @@ func run(cmd *cobra.Command, args []string) { return } - logger.Init() + logger.Init(stdio) defer logger.Sync() // Setup context with cancellation for graceful shutdown diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..85b86cc8 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,78 @@ + +# Quickstart Guide for KAgnet Tools + +## About this guide + +This guide provides a quick overview of how to set up and run KAgent tools using AgentGateway. + +For more detaled information on KAgent tools, please refer to the [KAgent Tools Documentation](https://kagent.dev/tools). + +To learn more about agentgateway, see [AgentGateway](https://agentgateway.dev/docs/about/) + +### Running KAgent Tools using AgentGateway + +1. Download tools binary and install it. +2. Download tools configuration file for agentgateway. +3. Download the agentgateway binary and install it. +4. Run the agentgateway with the configuration file. +5. open http://localhost:15000/ui + +```bash +curl -sL https://raw.githubusercontent.com/kagent-dev/tools/refs/heads/main/scripts/install.sh | bash +curl -sL https://raw.githubusercontent.com/kagent-dev/tools/refs/heads/main/scripts/agentgateway-config-tools.yaml +curl -sL https://raw.githubusercontent.com/agentgateway/agentgateway/refs/heads/main/common/scripts/get-agentproxy | bash + +export PATH=$PATH:$HOME/.local/bin/ +agentgateway -f agentgateway-config-tools.yaml +``` + +agentgateway-config-tools.yaml: +```yaml +binds: + - port: 30805 + listeners: + - routes: + - policies: + cors: + allowOrigins: + - "*" + allowHeaders: + - mcp-protocol-version + - content-type + backends: + - mcp: + name: default + targets: + - name: kagent-tools + stdio: + cmd: kagent-tools + args: ["--stdio", "--kubeconfig", "~/.kube/config"] +``` +Afterwards, you can run it with make command +```bash +make run-agentgateway +``` + +### Running KAgent Tools using Cursor MCP + + +1. Download the agentgateway binary and install it. +``` +curl -sL https://raw.githubusercontent.com/kagent-dev/tools/refs/heads/main/scripts/install.sh | bash +``` + +2. Create `.cursor/mcp.json` + +```json +{ + "mcpServers": { + "kagent-tools": { + "command": "kagent-tools", + "args": ["--stdio", "--kubeconfig", "~/.kube/config"] + } + } +} +``` + + + diff --git a/helm/kagent-tools/Chart-template.yaml b/helm/kagent-tools/Chart-template.yaml index 6fb1a689..3d2eb2b2 100644 --- a/helm/kagent-tools/Chart-template.yaml +++ b/helm/kagent-tools/Chart-template.yaml @@ -1,5 +1,6 @@ apiVersion: v2 name: kagent-tools -description: A Helm chart for kagent-tools, +description: A Helm chart for kagent-tools type: application version: ${VERSION} +appVersion: ${VERSION} \ No newline at end of file diff --git a/internal/commands/builder.go b/internal/commands/builder.go index f3e75ce2..e0ec4774 100644 --- a/internal/commands/builder.go +++ b/internal/commands/builder.go @@ -15,6 +15,13 @@ import ( "go.opentelemetry.io/otel/attribute" ) +const ( + // DefaultTimeout is the default timeout for command execution + DefaultTimeout = 2 * time.Minute + // DefaultCacheTTL is the default cache TTL + DefaultCacheTTL = 1 * time.Minute +) + // CommandBuilder provides a fluent interface for building CLI commands type CommandBuilder struct { command string @@ -43,10 +50,10 @@ func NewCommandBuilder(command string) *CommandBuilder { args: make([]string, 0), labels: make(map[string]string), annotations: make(map[string]string), - timeout: 60 * time.Second, + timeout: DefaultTimeout, useTimeout: false, // Only enable timeout when explicitly requested validate: true, - cacheTTL: 1 * time.Minute, + cacheTTL: DefaultCacheTTL, } } diff --git a/internal/commands/builder_test.go b/internal/commands/builder_test.go index 52dc0ecf..377afeb5 100644 --- a/internal/commands/builder_test.go +++ b/internal/commands/builder_test.go @@ -19,8 +19,8 @@ func TestNewCommandBuilder(t *testing.T) { assert.Empty(t, cb.output) assert.NotNil(t, cb.labels) assert.NotNil(t, cb.annotations) - assert.Equal(t, 60*time.Second, cb.timeout) - assert.Equal(t, 1*time.Minute, cb.cacheTTL) + assert.Equal(t, DefaultTimeout, cb.timeout) + assert.Equal(t, DefaultCacheTTL, cb.cacheTTL) assert.True(t, cb.validate) assert.False(t, cb.cached) assert.False(t, cb.dryRun) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 041d499c..b9a078f2 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -10,23 +10,39 @@ import ( var globalLogger *slog.Logger -func Init() { +// Init initializes the global logger +// If useStderr is true, logs will be written to stderr (for stdio mode) +// If useStderr is false, logs will be written to stdout (for HTTP mode) +func Init(useStderr bool) { opts := &slog.HandlerOptions{ Level: slog.LevelInfo, } + // Choose output destination based on mode + output := os.Stdout + if useStderr { + output = os.Stderr + } + if os.Getenv("KAGENT_LOG_FORMAT") == "json" { - globalLogger = slog.New(slog.NewJSONHandler(os.Stdout, opts)) + globalLogger = slog.New(slog.NewJSONHandler(output, opts)) } else { - globalLogger = slog.New(slog.NewTextHandler(os.Stdout, opts)) + globalLogger = slog.New(slog.NewTextHandler(output, opts)) } slog.SetDefault(globalLogger) } +// InitWithEnv initializes the logger using environment variables +// This is a convenience function that defaults to stdout unless KAGENT_USE_STDERR is set +func InitWithEnv() { + useStderr := os.Getenv("KAGENT_USE_STDERR") == "true" + Init(useStderr) +} + func Get() *slog.Logger { if globalLogger == nil { - Init() + InitWithEnv() } return globalLogger } diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index ad5c9889..efca71e7 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -64,7 +64,8 @@ func TestGet(t *testing.T) { } func TestInit(t *testing.T) { - assert.NotPanics(t, Init) + assert.NotPanics(t, func() { Init(false) }) + assert.NotPanics(t, func() { Init(true) }) } func TestSync(t *testing.T) { diff --git a/scripts/agentgateway-config-tools.yaml b/scripts/agentgateway-config-tools.yaml new file mode 100644 index 00000000..67863bb3 --- /dev/null +++ b/scripts/agentgateway-config-tools.yaml @@ -0,0 +1,23 @@ +binds: +- port: 30805 + listeners: + - routes: + - backends: + - mcp: + name: default + targets: + - name: kagent-tools + stdio: + cmd: kagent-tools + args: + - --stdio + - --kubeconfig + - ~/.kube/config + policies: + cors: + allowOrigins: + - '*' + allowHeaders: + - mcp-protocol-version + - content-type + - cache-control From 7425a6ca7411b3971d5306fbc97ba8604a342aca Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Mon, 28 Jul 2025 16:53:08 +0200 Subject: [PATCH 21/41] set json format optional (#16) * set json format optional Signed-off-by: Dmytro Rashko * set json format optional Signed-off-by: Dmytro Rashko * set json format optional Signed-off-by: Dmytro Rashko --------- Signed-off-by: Dmytro Rashko --- .gitignore | 1 + Makefile | 2 +- internal/commands/builder.go | 6 +++--- internal/commands/builder_test.go | 6 +++--- pkg/k8s/k8s.go | 6 +++--- pkg/k8s/k8s_test.go | 12 +++++++----- test/e2e/cli_test.go | 2 +- test/e2e/helpers_test.go | 4 ++-- 8 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 6bd4b641..4b3d33a1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ bin/ *.html /helm/kagent-tools/Chart.yaml /reports/tools-cve.csv +.dagger/ diff --git a/Makefile b/Makefile index 8f9fcf97..f16920fb 100644 --- a/Makefile +++ b/Makefile @@ -138,7 +138,7 @@ DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUI # tools image build args TOOLS_ISTIO_VERSION ?= 1.26.2 TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 -TOOLS_KUBECTL_VERSION ?= 1.33.2 +TOOLS_KUBECTL_VERSION ?= 1.33.3 TOOLS_HELM_VERSION ?= 3.18.4 TOOLS_CILIUM_VERSION ?= 0.18.5 diff --git a/internal/commands/builder.go b/internal/commands/builder.go index e0ec4774..3bced945 100644 --- a/internal/commands/builder.go +++ b/internal/commands/builder.go @@ -445,7 +445,7 @@ func GetPods(namespace string, labels map[string]string) *CommandBuilder { builder = builder.WithLabels(labels) } - return builder.WithCache(true).WithOutput("json") + return builder.WithCache(true) } // GetServices creates a command to get services @@ -460,7 +460,7 @@ func GetServices(namespace string, labels map[string]string) *CommandBuilder { builder = builder.WithLabels(labels) } - return builder.WithCache(true).WithOutput("json") + return builder.WithCache(true) } // GetDeployments creates a command to get deployments @@ -475,7 +475,7 @@ func GetDeployments(namespace string, labels map[string]string) *CommandBuilder builder = builder.WithLabels(labels) } - return builder.WithCache(true).WithOutput("json") + return builder.WithCache(true) } // DescribeResource creates a command to describe a resource diff --git a/internal/commands/builder_test.go b/internal/commands/builder_test.go index 377afeb5..f8a98fc2 100644 --- a/internal/commands/builder_test.go +++ b/internal/commands/builder_test.go @@ -253,7 +253,7 @@ func TestGetPods(t *testing.T) { assert.Equal(t, namespace, cb.namespace) assert.Equal(t, labels, cb.labels) assert.True(t, cb.cached) - assert.Equal(t, "json", cb.output) + assert.Empty(t, cb.output) // No default output format } func TestGetServices(t *testing.T) { @@ -268,7 +268,7 @@ func TestGetServices(t *testing.T) { assert.Equal(t, namespace, cb.namespace) assert.Equal(t, labels, cb.labels) assert.True(t, cb.cached) - assert.Equal(t, "json", cb.output) + assert.Empty(t, cb.output) // No default output format } func TestGetDeployments(t *testing.T) { @@ -283,7 +283,7 @@ func TestGetDeployments(t *testing.T) { assert.Equal(t, namespace, cb.namespace) assert.Equal(t, labels, cb.labels) assert.True(t, cb.cached) - assert.Equal(t, "json", cb.output) + assert.Empty(t, cb.output) // No default output format } func TestDescribeResource(t *testing.T) { diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index e474df61..c8e90852 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -58,7 +58,7 @@ func (k *K8sTool) handleKubectlGetEnhanced(ctx context.Context, request mcp.Call resourceName := mcp.ParseString(request, "resource_name", "") namespace := mcp.ParseString(request, "namespace", "") allNamespaces := mcp.ParseString(request, "all_namespaces", "") == "true" - output := mcp.ParseString(request, "output", "json") + output := mcp.ParseString(request, "output", "wide") if resourceType == "" { return mcp.NewToolResultError("resource_type parameter is required"), nil @@ -292,7 +292,7 @@ func (k *K8sTool) handleExecCommand(ctx context.Context, request mcp.CallToolReq // Get available API resources func (k *K8sTool) handleGetAvailableAPIResources(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return k.runKubectlCommand(ctx, "api-resources", "-o", "json") + return k.runKubectlCommand(ctx, "api-resources") } // Kubectl describe tool @@ -567,7 +567,7 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { mcp.WithString("resource_name", mcp.Description("Name of specific resource (optional)")), mcp.WithString("namespace", mcp.Description("Namespace to query (optional)")), mcp.WithString("all_namespaces", mcp.Description("Query all namespaces (true/false)")), - mcp.WithString("output", mcp.Description("Output format (json, yaml, wide, etc.)")), + mcp.WithString("output", mcp.Description("Output format (json, yaml, wide)"), mcp.DefaultString("wide")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_resources", k8sTool.handleKubectlGetEnhanced))) s.AddTool(mcp.NewTool("k8s_get_pod_logs", diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go index f3c53c4b..e3730663 100644 --- a/pkg/k8s/k8s_test.go +++ b/pkg/k8s/k8s_test.go @@ -37,8 +37,10 @@ func TestHandleGetAvailableAPIResources(t *testing.T) { t.Run("success", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - expectedOutput := `[{"name": "pods", "singularName": "pod", "namespaced": true, "kind": "Pod"}]` - mock.AddCommandString("kubectl", []string{"api-resources", "-o", "json"}, expectedOutput, nil) + expectedOutput := `NAME SHORTNAMES APIVERSION NAMESPACED KIND +pods po v1 true Pod +services svc v1 true Service` + mock.AddCommandString("kubectl", []string{"api-resources"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -56,7 +58,7 @@ func TestHandleGetAvailableAPIResources(t *testing.T) { t.Run("kubectl command failure", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - mock.AddCommandString("kubectl", []string{"api-resources", "-o", "json"}, "", assert.AnError) + mock.AddCommandString("kubectl", []string{"api-resources"}, "", assert.AnError) ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() @@ -408,8 +410,8 @@ func TestHandleKubectlGetEnhanced(t *testing.T) { t.Run("valid resource_type", func(t *testing.T) { mock := cmd.NewMockShellExecutor() - expectedOutput := `{"items": [{"metadata": {"name": "pod1"}}]}` - mock.AddCommandString("kubectl", []string{"get", "pods", "-o", "json"}, expectedOutput, nil) + expectedOutput := `NAME READY STATUS RESTARTS AGE` + mock.AddCommandString("kubectl", []string{"get", "pods", "-o", "wide"}, expectedOutput, nil) ctx := cmd.WithShellExecutor(ctx, mock) k8sTool := newTestK8sTool() diff --git a/test/e2e/cli_test.go b/test/e2e/cli_test.go index 61285b06..73df2a85 100644 --- a/test/e2e/cli_test.go +++ b/test/e2e/cli_test.go @@ -44,7 +44,7 @@ var _ = Describe("KAgent Tools E2E Tests", func() { config := TestServerConfig{ Port: 8085, Stdio: false, - Timeout: 30 * time.Second, + Timeout: 60 * time.Second, } server := NewTestServer(config) diff --git a/test/e2e/helpers_test.go b/test/e2e/helpers_test.go index 59008d85..f88b6aba 100644 --- a/test/e2e/helpers_test.go +++ b/test/e2e/helpers_test.go @@ -249,8 +249,8 @@ func InstallKAgentTools(namespace string, releaseName string) { // GetMCPClient creates a new MCP client configured for the e2e test environment using the official mcp-go client func GetMCPClient() (*MCPClient, error) { - // Create HTTP transport for the MCP server - httpTransport, err := transport.NewStreamableHTTP("http://127.0.0.1:30885/mcp", transport.WithHTTPTimeout(15*time.Second)) + // Create HTTP transport for the MCP server with timeout long enough for operations like Istio installation + httpTransport, err := transport.NewStreamableHTTP("http://127.0.0.1:30885/mcp", transport.WithHTTPTimeout(180*time.Second)) if err != nil { return nil, fmt.Errorf("failed to create HTTP transport: %w", err) } From 0aec6932360c37ff2ae9a66b01f66de230765ea6 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Wed, 30 Jul 2025 00:07:01 +0200 Subject: [PATCH 22/41] fix version and makefile ENV var (#18) Signed-off-by: Dmytro Rashko --- Makefile | 6 +++--- README.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index f16920fb..835e14ce 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ LDFLAGS := -X github.com/kagent-dev/tools/internal/version.Version=$(VERSION) -X ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin -PATH := $HOME/local/bin:/opt/homebrew/bin/:$(LOCALBIN):$(PATH) +PATH := $(HOME)/local/bin:/opt/homebrew/bin/:$(LOCALBIN):$(PATH) HELM_DIST_FOLDER ?= $(shell pwd)/dist .PHONY: clean @@ -214,10 +214,10 @@ otel-local: .PHONY: tools-install tools-install: clean - mkdir -p $HOME/.local/bin + mkdir -p $(HOME)/.local/bin go build -ldflags "$(LDFLAGS)" -o $(LOCALBIN)/kagent-tools ./cmd go build -ldflags "$(LDFLAGS)" -o $(HOME)/.local/bin/kagent-tools ./cmd - $HOME/.local/bin/kagent-tools --version + $(HOME)/.local/bin/kagent-tools --version .PHONY: run-agentgateway run-agentgateway: tools-install diff --git a/README.md b/README.md index 0bc29beb..a801cb1c 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,13 @@ curl -sL https://raw.githubusercontent.com/kagent-dev/tools/refs/heads/main/scri - **Docker:** ```bash -docker run -it --rm -p 8084:8084 ghcr.io/kagent-dev/kagent/tools:0.0.10 +docker run -it --rm -p 8084:8084 ghcr.io/kagent-dev/kagent/tools:0.0.11 ``` - **Kubernetes** ```bash -helm upgrade -i -n kagent --create-namespace kagent-tools oci://ghcr.io/kagent-dev/tools/helm/kagent-tools --version 0.0.10 +helm upgrade -i -n kagent --create-namespace kagent-tools oci://ghcr.io/kagent-dev/tools/helm/kagent-tools --version 0.0.11 helm ls -A ``` From 6d4dab9744e90473aebd41275e8084663fa2e8dc Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Thu, 25 Sep 2025 15:53:19 +0200 Subject: [PATCH 23/41] updated dependencies (#25) * updated dependencies Signed-off-by: Dmytro Rashko * ci go-version: "1.25" Signed-off-by: Dmytro Rashko * fix agentgateway config Signed-off-by: Dmytro Rashko --------- Signed-off-by: Dmytro Rashko --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 12 +- .github/workflows/ci.yaml | 4 +- Makefile | 10 +- README.md | 4 +- go.mod | 112 ++--- go.sum | 631 +++++++++++++++++++------ scripts/agentgateway-config-tools.yaml | 1 - 8 files changed, 532 insertions(+), 244 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f3be754e..141f5b91 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ ARG TOOLS_GO_VERSION -FROM mcr.microsoft.com/devcontainers/go:1-${TOOLS_GO_VERSION}-bookworm +FROM mcr.microsoft.com/devcontainers/go:dev-${TOOLS_GO_VERSION}-bookworm RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 22b0a48f..0d75eec0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,12 +3,12 @@ "build": { "dockerfile": "Dockerfile", "args": { - "TOOLS_GO_VERSION": "1.24", - "TOOLS_HELM_VERSION": "3.18.3", - "TOOLS_ISTIO_VERSION": "1.26.2", - "TOOLS_KUBECTL_VERSION": "1.33.2", + "TOOLS_GO_VERSION": "1.25", + "TOOLS_HELM_VERSION": "3.19.0", + "TOOLS_ISTIO_VERSION": "1.27.1", + "TOOLS_KUBECTL_VERSION": "1.34.1", "TOOLS_ARGO_ROLLOUTS_VERSION": "1.8.3", - "TOOLS_CILIUM_VERSION": "0.18.5" + "TOOLS_CILIUM_VERSION": "0.18.7" } }, "features": { @@ -49,7 +49,7 @@ "forwardPorts": [8084], //network - "network": "host", + // "network": "host", //mount docker directly on the host "mounts": ["source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"], diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 72a6e43b..c6b08edc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,7 +47,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: "1.24" + go-version: "1.25" cache: true - name: Run cmd/main.go tests @@ -64,7 +64,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: "1.24" + go-version: "1.25" cache: true - name: Create k8s Kind Cluster diff --git a/Makefile b/Makefile index 835e14ce..f010ebca 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ HELM_REPO ?= oci://ghcr.io/kagent-dev HELM_ACTION=upgrade --install KIND_CLUSTER_NAME ?= kagent -KIND_IMAGE_VERSION ?= 1.33.1 +KIND_IMAGE_VERSION ?= 1.34.0 KIND_CREATE_CMD ?= "kind create cluster --name $(KIND_CLUSTER_NAME) --image kindest/node:v$(KIND_IMAGE_VERSION) --config ./scripts/kind/kind-config.yaml" BUILD_DATE := $(shell date -u '+%Y-%m-%d') @@ -136,11 +136,11 @@ DOCKER_BUILDER ?= docker buildx DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUILDX_BUILDER_NAME) # tools image build args -TOOLS_ISTIO_VERSION ?= 1.26.2 +TOOLS_ISTIO_VERSION ?= 1.27.1 TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 -TOOLS_KUBECTL_VERSION ?= 1.33.3 -TOOLS_HELM_VERSION ?= 3.18.4 -TOOLS_CILIUM_VERSION ?= 0.18.5 +TOOLS_KUBECTL_VERSION ?= 1.34.1 +TOOLS_HELM_VERSION ?= 3.19.0 +TOOLS_CILIUM_VERSION ?= 0.18.7 # build args TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION) diff --git a/README.md b/README.md index a801cb1c..0218c692 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,13 @@ curl -sL https://raw.githubusercontent.com/kagent-dev/tools/refs/heads/main/scri - **Docker:** ```bash -docker run -it --rm -p 8084:8084 ghcr.io/kagent-dev/kagent/tools:0.0.11 +docker run -it --rm -p 8084:8084 ghcr.io/kagent-dev/kagent/tools:0.0.12 ``` - **Kubernetes** ```bash -helm upgrade -i -n kagent --create-namespace kagent-tools oci://ghcr.io/kagent-dev/tools/helm/kagent-tools --version 0.0.11 +helm upgrade -i -n kagent --create-namespace kagent-tools oci://ghcr.io/kagent-dev/tools/helm/kagent-tools --version 0.0.12 helm ls -A ``` diff --git a/go.mod b/go.mod index cde53c82..0be0049e 100644 --- a/go.mod +++ b/go.mod @@ -1,93 +1,63 @@ module github.com/kagent-dev/tools -go 1.24.5 +go 1.25.1 require ( github.com/joho/godotenv v1.5.1 - github.com/mark3labs/mcp-go v0.32.0 - github.com/onsi/ginkgo/v2 v2.23.4 - github.com/onsi/gomega v1.37.0 - github.com/spf13/cobra v1.9.1 - github.com/stretchr/testify v1.10.0 - github.com/testcontainers/testcontainers-go v0.38.0 + github.com/mark3labs/mcp-go v0.40.0 + github.com/onsi/ginkgo/v2 v2.25.3 + github.com/onsi/gomega v1.38.2 + github.com/spf13/cobra v1.10.1 + github.com/stretchr/testify v1.11.1 github.com/tmc/langchaingo v0.1.13 - go.opentelemetry.io/otel v1.37.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 - go.opentelemetry.io/otel/metric v1.37.0 - go.opentelemetry.io/otel/sdk v1.37.0 - go.opentelemetry.io/otel/trace v1.37.0 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 + go.opentelemetry.io/otel/metric v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 ) require ( - dario.cat/mergo v1.0.1 // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect - github.com/containerd/errdefs v1.0.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/chzyer/readline v1.5.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect - github.com/docker/docker v28.2.2+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/ebitengine/purego v0.8.4 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/go-archive v0.1.0 // indirect - github.com/moby/patternmatcher v0.6.0 // indirect - github.com/moby/sys/sequential v0.6.0 // indirect - github.com/moby/sys/user v0.4.0 // indirect - github.com/moby/sys/userns v0.1.0 // indirect - github.com/moby/term v0.5.0 // indirect - github.com/morikuni/aec v1.0.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pkoukk/tiktoken-go v0.1.6 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/pkoukk/tiktoken-go v0.1.8 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/shirou/gopsutil/v4 v4.25.5 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cast v1.9.2 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.8.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect - golang.org/x/tools v0.33.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/grpc v1.73.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 9966e3ec..57006e99 100644 --- a/go.sum +++ b/go.sum @@ -1,48 +1,175 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY= +cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E= +cloud.google.com/go/ai v0.7.0 h1:P6+b5p4gXlza5E+u7uvcgYlzZ7103ACg70YdZeC6oGE= +cloud.google.com/go/ai v0.7.0/go.mod h1:7ozuEcraovh4ABsPbrec3o4LmFl9HigNI3D5haxYeQo= +cloud.google.com/go/aiplatform v1.68.0 h1:EPPqgHDJpBZKRvv+OsB3cr0jYz3EL2pZ+802rBPcG8U= +cloud.google.com/go/aiplatform v1.68.0/go.mod h1:105MFA3svHjC3Oazl7yjXAmIR89LKhRAeNdnDKJczME= +cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw= +cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= +cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= +cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +cloud.google.com/go/vertexai v0.12.0 h1:zTadEo/CtsoyRXNx3uGCncoWAP1H2HakGqwznt+iMo8= +cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AssemblyAI/assemblyai-go-sdk v1.3.0 h1:AtOVgGxUycvK4P4ypP+1ZupecvFgnfH+Jsum0o5ILoU= +github.com/AssemblyAI/assemblyai-go-sdk v1.3.0/go.mod h1:H0naZbvpIW49cDA5ZZ/gggeXqi7ojSGB1mqshRk6kNE= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/Code-Hex/go-generics-cache v1.3.1 h1:i8rLwyhoyhaerr7JpjtYjJZUcCbWOdiYO3fZXLiEC4g= +github.com/Code-Hex/go-generics-cache v1.3.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/IBM/watsonx-go v1.0.0 h1:xG7xA2W9N0RsiztR26dwBI8/VxIX4wTBhdYmEis2Yl8= +github.com/IBM/watsonx-go v1.0.0/go.mod h1:8lzvpe/158JkrzvcoIcIj6OdNty5iC9co5nQHfkhRtM= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= +github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/amikos-tech/chroma-go v0.1.2 h1:ECiJ4Gn0AuJaj/jLo+FiqrKRHBVDkrDaUQVRBsEMmEQ= +github.com/amikos-tech/chroma-go v0.1.2/go.mod h1:R/RUp0aaqCWdSXWyIUTfjuNymwqBGLYFgXNZEmisphY= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= +github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= +github.com/antchfx/xmlquery v1.3.17 h1:d0qWjPp/D+vtRw7ivCwT5ApH/3CkQU8JOeo3245PpTk= +github.com/antchfx/xmlquery v1.3.17/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA= +github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY= +github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= +github.com/aws/aws-sdk-go-v2/config v1.27.12 h1:vq88mBaZI4NGLXk8ierArwSILmYHDJZGJOeAc/pzEVQ= +github.com/aws/aws-sdk-go-v2/config v1.27.12/go.mod h1:IOrsf4IiN68+CgzyuyGUYTpCrtUQTbbMEAtR/MR/4ZU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.12 h1:PVbKQ0KjDosI5+nEdRMU8ygEQDmkJTSHBqPjEX30lqc= +github.com/aws/aws-sdk-go-v2/credentials v1.17.12/go.mod h1:jlWtGFRtKsqc5zqerHZYmKmRkUXo3KPM14YJ13ZEjwE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.1 h1:vTHgBjsGhgKWWIgioxd7MkBH5Ekr8C6Cb+/8iWf1dpc= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.1/go.mod h1:nZspkhg+9p8iApLFoyAqfyuMP0F38acy2Hm3r5r95Cg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.5 h1:Ciiz/plN+Z+pPO1G0W2zJoYIIl0KtKzY0LJ78NXYTws= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.5/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= +github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= +github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f h1:6jduT9Hfc0njg5jJ1DdKCFPdMBrp/mdZfCpa5h+WM74= +github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= +github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cohere-ai/tokenizer v1.1.2 h1:t3KwUBSpKiBVFtpnHBfVIQNmjfZUuqFVYuSFkZYOWpU= +github.com/cohere-ai/tokenizer v1.1.2/go.mod h1:9MNFPd9j1fuiEK3ua2HSCUxxcrfGMlSqpa93livg/C0= +github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes= +github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= -github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/deepmap/oapi-codegen/v2 v2.1.0 h1:I/NMVhJCtuvL9x+S2QzZKpSjGi33oDZwPRdemvOZWyQ= +github.com/deepmap/oapi-codegen/v2 v2.1.0/go.mod h1:R1wL226vc5VmCNJUvMyYr3hJMm5reyv25j952zAVXZ8= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= -github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= +github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/gage-technologies/mistral-go v1.1.0 h1:POv1wM9jA/9OBXGV2YdPi9Y/h09+MjCbUF+9hRYlVUI= +github.com/gage-technologies/mistral-go v1.1.0/go.mod h1:tF++Xt7U975GcLlzhrjSQb8l/x+PrriO9QEdsgm9l28= +github.com/getsentry/sentry-go v0.12.0 h1:era7g0re5iY13bHSdN/xMkyV+5zZppjRVQhZrXCaEIk= +github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= +github.com/getzep/zep-go v1.0.4 h1:09o26bPP2RAPKFjWuVWwUWLbtFDF/S8bfbilxzeZAAg= +github.com/getzep/zep-go v1.0.4/go.mod h1:HC1Gz7oiyrzOTvzeKC4dQKUiUy87zpIJl0ZFXXdHuss= +github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -50,198 +177,390 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/analysis v0.21.2 h1:hXFrOYFHUAMQdu6zwAiKKJHJQ8kqZs1ux/ru1P1wLJU= +github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/loads v0.21.1 h1:Wb3nVZpdEzDTcly8S4HMkey6fjARRzb7iEaySimlDW0= +github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/validate v0.21.0 h1:+Wqk39yKOhfpLqNLEC0/eViCkzM5FVXVqrvt526+wcI= +github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI= +github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/generative-ai-go v0.15.1 h1:n8aQUpvhPOlGVuM2DRkJ2jvx04zpp42B778AROJa+pQ= +github.com/google/generative-ai-go v0.15.1/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 h1:ZI8gCoCjGzPsum4L21jHdQs8shFBIQih1TM9Rd/c+EQ= +github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= +github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= +github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= +github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8= -github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.40.0 h1:M0oqK412OHBKut9JwXSsj4KanSmEKpzoW8TcxoPOkAU= +github.com/mark3labs/mcp-go v0.40.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/metaphorsystems/metaphor-go v0.0.0-20230816231421-43794c04824e h1:4N462rhrxy7KezYYyL3RjJPWlhXiSkfFes0YsMqicd0= +github.com/metaphorsystems/metaphor-go v0.0.0-20230816231421-43794c04824e/go.mod h1:mDz8kHE7x6Ja95drCQ2T1vLyPRc/t69Cf3wau91E3QU= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/milvus-io/milvus-proto/go-api/v2 v2.3.5 h1:4XDy6ATB2Z0fl4Jn0hS6BT6/8YaE0d+ZUf4uBH+Z0Do= +github.com/milvus-io/milvus-proto/go-api/v2 v2.3.5/go.mod h1:1OIl0v5PQeNxIJhCvY+K55CBUOYDZevw9g9380u1Wek= +github.com/milvus-io/milvus-sdk-go/v2 v2.3.6 h1:JVn9OdaronLGmtpxvamQf523mtn3Z/CRxkSZCMWutV4= +github.com/milvus-io/milvus-sdk-go/v2 v2.3.6/go.mod h1:bYFSXVxEj6A/T8BfiR+xkofKbAVZpWiDvKr3SzYUWiA= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= +github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= +github.com/nlpodyssey/cybertron v0.2.1 h1:zBvzmjP6Teq3u8yiHuLoUPxan6ZDRq/32GpV6Ep8X08= +github.com/nlpodyssey/cybertron v0.2.1/go.mod h1:Vg9PeB8EkOTAgSKQ68B3hhKUGmB6Vs734dBdCyE4SVM= +github.com/nlpodyssey/gopickle v0.2.0 h1:4naD2DVylYJupQLbCQFdwo6yiXEmPyp+0xf5MVlrBDY= +github.com/nlpodyssey/gopickle v0.2.0/go.mod h1:YIUwjJ2O7+vnBsxUN+MHAAI3N+adqEGiw+nDpwW95bY= +github.com/nlpodyssey/gotokenizers v0.2.0 h1:CWx/sp9s35XMO5lT1kNXCshFGDCfPuuWdx/9JiQBsVc= +github.com/nlpodyssey/gotokenizers v0.2.0/go.mod h1:SBLbuSQhpni9M7U+Ie6O46TXYN73T2Cuw/4eeYHYJ+s= +github.com/nlpodyssey/spago v1.1.0 h1:DGUdGfeGR7TxwkYRdSEzbSvunVWN5heNSksmERmj97w= +github.com/nlpodyssey/spago v1.1.0/go.mod h1:jDWGZwrB4B61U6Tf3/+MVlWOtNsk3EUA7G13UDHlnjQ= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw= +github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M= +github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pgvector/pgvector-go v0.1.1 h1:kqJigGctFnlWvskUiYIvJRNwUtQl/aMSUZVs0YWQe+g= +github.com/pgvector/pgvector-go v0.1.1/go.mod h1:wLJgD/ODkdtd2LJK4l6evHXTuG+8PxymYAVomKHOWac= +github.com/pinecone-io/go-pinecone v0.4.1 h1:hRJgtGUIHwvM1NvzKe+YXog4NxYi9x3NdfFhQ2QWBWk= +github.com/pinecone-io/go-pinecone v0.4.1/go.mod h1:KwWSueZFx9zccC+thBk13+LDiOgii8cff9bliUI4tQs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo= +github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/redis/rueidis v1.0.34 h1:cdggTaDDoqLNeoKMoew8NQY3eTc83Kt6XyfXtoCO2Wc= +github.com/redis/rueidis v1.0.34/go.mod h1:g8nPmgR4C68N3abFiOc/gUOSEKw3Tom6/teYMehg4RE= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc= -github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= -github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw= -github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= +github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= +github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U= +github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI= +github.com/testcontainers/testcontainers-go/modules/chroma v0.31.0 h1:fB/04gfZ9iqm9FO6tEgB8RKU/Dbkc1Opdhp47uiCDSM= +github.com/testcontainers/testcontainers-go/modules/chroma v0.31.0/go.mod h1:dYvKTWVnJ58YizDYX2txYwDG4FvudYUmx37tvbza90o= +github.com/testcontainers/testcontainers-go/modules/milvus v0.31.0 h1:0wTakit4o9Yn0VNkzDOY5hV1LeKcw2W7gxcLa3el2x0= +github.com/testcontainers/testcontainers-go/modules/milvus v0.31.0/go.mod h1:ta9EDZd+lKBMU7enljbNu5H1G495fnT0dw7hmsCPWa0= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.31.0 h1:0ZAEX50NNK/TVRqDls4aQUmokRcYzstKzmF3DCfFK+Y= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.31.0/go.mod h1:n5KbYAdzD8xJrNVGdPvSacJtwZ4D0Q/byTMI5vR/dk8= +github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0 h1:790+S8ewZYCbG+o8IiFlZ8ZZ33XbNO6zV9qhU6xhlRk= +github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0/go.mod h1:REFmO+lSG9S6uSBEwIMZCxeI36uhScjTwChYADeO3JA= +github.com/testcontainers/testcontainers-go/modules/opensearch v0.31.0 h1:sgo2PJb8oCK7ogJjRxAkidXmt+gPzwtyhZpaxSI5wDo= +github.com/testcontainers/testcontainers-go/modules/opensearch v0.31.0/go.mod h1:l4Z7QqGpdk4wTTQk8J8CZ75pfqAz1dizm+LECOLuNVw= +github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 h1:isAwFS3KNKRbJMbWv+wolWqOFUECmjYZ+sIRZCIBc/E= +github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0/go.mod h1:ZNYY8vumNCEG9YI59A9d6/YaMY49uwRhmeU563EzFGw= +github.com/testcontainers/testcontainers-go/modules/qdrant v0.31.0 h1:5bYvi8lSqDnJrO1w5W3AFaSsRe4ZDv4TPj1tsaBEz20= +github.com/testcontainers/testcontainers-go/modules/qdrant v0.31.0/go.mod h1:/3GyFMTSiem1j5mfI/96MufdNvB3A8Xqa+xnV4CUR4A= +github.com/testcontainers/testcontainers-go/modules/redis v0.31.0 h1:5X6GhOdLwV86zcW8sxppJAMtsDC9u+r9tb3biBc9GKs= +github.com/testcontainers/testcontainers-go/modules/redis v0.31.0/go.mod h1:dKi5xBwy1k4u8yb3saQHu7hMEJwewHXxzbcMAuLiA6o= +github.com/testcontainers/testcontainers-go/modules/weaviate v0.31.0 h1:iVJX9O12GHRhqPgIuz/eE8BsNEwyrUMJnWgduBt8quc= +github.com/testcontainers/testcontainers-go/modules/weaviate v0.31.0/go.mod h1:WNc2XhLphiLdNJdjJZvUtRj08ThLY8FL60y7FQSJTPQ= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= +github.com/weaviate/weaviate v1.24.1 h1:Cl/NnqgFlNfyC7KcjFtETf1bwtTQPLF3oz5vavs+Jq0= +github.com/weaviate/weaviate v1.24.1/go.mod h1:wcg1vJgdIQL5MWBN+871DFJQa+nI2WzyXudmGjJ8cG4= +github.com/weaviate/weaviate-go-client/v4 v4.13.1 h1:7PuK/hpy6Q0b9XaVGiUg5OD1MI/eF2ew9CJge9XdBEE= +github.com/weaviate/weaviate-go-client/v4 v4.13.1/go.mod h1:B2m6g77xWDskrCq1GlU6CdilS0RG2+YXEgzwXRADad0= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 h1:K+bMSIx9A7mLES1rtG+qKduLIXq40DAzYHtb0XuCukA= +gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow= +gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 h1:oYrL81N608MLZhma3ruL8qTM4xcpYECGut8KSxRY59g= +gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8= +gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a h1:O85GKETcmnCNAfv4Aym9tepU8OE0NmcZNqPlXcsBKBs= +gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a/go.mod h1:LaSIs30YPGs1H5jwGgPhLzc8vkNc/k0rDX/fEZqiU/M= +gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84 h1:qqjvoVXdWIcZCLPMlzgA7P9FZWdPGPvP/l3ef8GzV6o= +gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84/go.mod h1:IJZ+fdMvbW2qW6htJx7sLJ04FEs4Ldl/MDsJtMKywfw= +gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f h1:Wku8eEdeJqIOFHtrfkYUByc4bCaTeA6fL0UJgfEiFMI= +gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f/go.mod h1:Tiuhl+njh/JIg0uS/sOJVYi0x2HEa5rc1OAaVsb5tAs= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.mongodb.org/mongo-driver/v2 v2.0.0 h1:Jfd7XpdZa9yk3eY774bO7SWVb30noLSirL9nKTpavhI= +go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 h1:jBpDk4HAUsrnVO1FsfCfCOTEc/MkInJmvfCHYLFiT80= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0/go.mod h1:H9LUIM1daaeZaz91vZcfeM0fejXPmgCYE8ZhzqfJuiU= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= -go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= +go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= +go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg= +go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE= +google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= +google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9 h1:jm6v6kMRpTYKxBRrDkYAitNJegUeO1Mf3Kt80obv0gg= +google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9/go.mod h1:LmwNphe5Afor5V3R5BppOULHOnt2mCIf+NxMd4XiygE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/scripts/agentgateway-config-tools.yaml b/scripts/agentgateway-config-tools.yaml index 67863bb3..ee53905a 100644 --- a/scripts/agentgateway-config-tools.yaml +++ b/scripts/agentgateway-config-tools.yaml @@ -4,7 +4,6 @@ binds: - routes: - backends: - mcp: - name: default targets: - name: kagent-tools stdio: From cd46de3574b7352505b56b405cd8cf89858bf946 Mon Sep 17 00:00:00 2001 From: sara Date: Tue, 21 Oct 2025 20:09:43 +0200 Subject: [PATCH 24/41] feat: add support for nodeSelector and tolerations (#23) Signed-off-by: Sara Qasmi Co-authored-by: Sara Qasmi --- .github/workflows/ci.yaml | 23 +++ Makefile | 6 + helm/kagent-tools/templates/deployment.yaml | 26 ++++ helm/kagent-tools/tests/deployment_test.yaml | 142 +++++++++++++++++++ helm/kagent-tools/values.yaml | 23 +++ 5 files changed, 220 insertions(+) create mode 100644 helm/kagent-tools/tests/deployment_test.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c6b08edc..0cb4edf0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -77,3 +77,26 @@ jobs: working-directory: . run: | make e2e + + helm-unit-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4.2.0 + with: + version: v3.17.0 + + - name: Install unittest plugin + run: | + helm plugin install https://github.com/helm-unittest/helm-unittest + + - name: Chart init + run: | + make helm-version + + - name: Run helm unit tests + run: | + make helm-test diff --git a/Makefile b/Makefile index f010ebca..ef6952ac 100644 --- a/Makefile +++ b/Makefile @@ -193,6 +193,12 @@ helm-install: helm-version helm-publish: helm-version helm push $(HELM_DIST_FOLDER)/kagent-tools-$(VERSION).tgz $(HELM_REPO)/tools/helm +.PHONY: helm-test +helm-test: helm-version + mkdir -p tmp + helm plugin ls | grep unittest || helm plugin install https://github.com/helm-unittest/helm-unittest.git + helm unittest helm/kagent-tools + .PHONY: create-kind-cluster create-kind-cluster: docker pull kindest/node:v$(KIND_IMAGE_VERSION) || true diff --git a/helm/kagent-tools/templates/deployment.yaml b/helm/kagent-tools/templates/deployment.yaml index 48ac0d77..ec8b8a80 100644 --- a/helm/kagent-tools/templates/deployment.yaml +++ b/helm/kagent-tools/templates/deployment.yaml @@ -23,6 +23,32 @@ spec: imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + affinity: + {{- with .Values.affinity }} + {{- toYaml . | nindent 8 }} + {{- end }} + + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- range . }} + - {{ toYaml . | nindent 10 | trim }} + {{- if not (hasKey . "labelSelector") }} + labelSelector: + matchLabels: + {{- include "kagent.selectorLabels" $ | nindent 14 }} + {{- end }} + {{- end }} + {{- end }} + + securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} serviceAccountName: {{ include "kagent.fullname" . }} diff --git a/helm/kagent-tools/tests/deployment_test.yaml b/helm/kagent-tools/tests/deployment_test.yaml new file mode 100644 index 00000000..397fd41a --- /dev/null +++ b/helm/kagent-tools/tests/deployment_test.yaml @@ -0,0 +1,142 @@ +suite: test controller deployment +templates: + - deployment.yaml +tests: + - it: should render deployment with default values + template: deployment.yaml + asserts: + - isKind: + of: Deployment + - equal: + path: metadata.name + value: RELEASE-NAME + - equal: + path: spec.replicas + value: 1 + - hasDocuments: + count: 1 + + - it: should render deployment with custom replica count + template: deployment.yaml + set: + replicaCount: 3 + asserts: + - equal: + path: spec.replicas + value: 3 + + - it: should have correct container image + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].name + value: tools + pattern: "^cr\\.kagent\\.dev/kagent-dev/kagent/tools:.+" + + - it: should use global tag when set + template: deployment.yaml + set: + tools: + image: + tag: "v1.0.0" + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: ghcr.io/kagent-dev/kagent/tools:v1.0.0 + + - it: should have correct resources + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].resources.requests.cpu + value: 100m + - equal: + path: spec.template.spec.containers[0].resources.requests.memory + value: 128Mi + - equal: + path: spec.template.spec.containers[0].resources.limits.cpu + value: 1000m + - equal: + path: spec.template.spec.containers[0].resources.limits.memory + value: 512Mi + + - it: should have correct service account name + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: RELEASE-NAME + + - it: should have correct container port + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].ports[0].containerPort + value: 8084 + + - it: should set nodeSelector + set: + nodeSelector: + role: AI + asserts: + - equal: + path: spec.template.spec.nodeSelector + value: + role: AI + + - it: should set tolerations + set: + tolerations: + - key: role + operator: Equal + value: AI + effect: NoSchedule + asserts: + - contains: + any: true + path: spec.template.spec.tolerations + content: + key: role + value: AI + effect: NoSchedule + operator: Equal + + - it: should render custom node affinity from values + set: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/e2e-az-name + operator: In + values: + - e2e-az1 + asserts: + - equal: + path: spec.template.spec.affinity + value: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/e2e-az-name + operator: In + values: + - e2e-az1 + + - it: should render topologySpreadConstraints with labelSelector fallback + set: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: zone + whenUnsatisfiable: ScheduleAnyway + asserts: + - equal: + path: spec.template.spec.topologySpreadConstraints[0].topologyKey + value: zone + - equal: + path: spec.template.spec.topologySpreadConstraints[0].labelSelector.matchLabels + value: + app.kubernetes.io/name: kagent-tools + app.kubernetes.io/instance: RELEASE-NAME diff --git a/helm/kagent-tools/values.yaml b/helm/kagent-tools/values.yaml index 6d68f55d..40262a7f 100644 --- a/helm/kagent-tools/values.yaml +++ b/helm/kagent-tools/values.yaml @@ -50,6 +50,29 @@ securityContext: {} # readOnlyRootFilesystem: true # runAsNonRoot: true # runAsUser: 1000 + +# Node taints which will be tolerated for Pod. +tolerations: [] + +# Node labels to match for Pod. +nodeSelector: {} + +# Affinity settings for Pod. +affinity: {} +# nodeAffinity: +# requiredDuringSchedulingIgnoredDuringExecution: +# nodeSelectorTerms: +# - matchExpressions: +# - key: kubernetes.io/e2e-az-name +# operator: In +# values: +# - e2e-az1 + +# Topology spread constraints for Pod. +topologySpreadConstraints: [] +# - maxSkew: 1 +# topologyKey: topology.kubernetes.io/zone +# whenUnsatisfiable: DoNotSchedule otel: tracing: From b34f97e4ac76edf6be7bea6e75bf9aa92b011c07 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Thu, 4 Dec 2025 16:53:41 +0100 Subject: [PATCH 25/41] Feature/bump dependencies (#32) * dependencies update Signed-off-by: Dmytro Rashko * readme Signed-off-by: Dmytro Rashko * go mod Signed-off-by: Dmytro Rashko * check latest GO version Signed-off-by: Dmytro Rashko * actions/setup-go@v6 Signed-off-by: Dmytro Rashko * actions/setup-go@v6 Signed-off-by: Dmytro Rashko --------- Signed-off-by: Dmytro Rashko --- .github/workflows/ci.yaml | 20 +- Makefile | 40 ++- README.md | 4 +- go.mod | 33 ++- go.sum | 513 ++++---------------------------------- 5 files changed, 115 insertions(+), 495 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0cb4edf0..017cfbac 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx @@ -42,13 +42,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v6 with: - go-version: "1.25" - cache: true + go-version: '^1.25.5' + cache: false - name: Run cmd/main.go tests working-directory: . @@ -59,13 +59,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v6 with: - go-version: "1.25" - cache: true + go-version: '^1.25.5' + cache: false - name: Create k8s Kind Cluster uses: helm/kind-action@v1 @@ -82,7 +82,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Helm uses: azure/setup-helm@v4.2.0 diff --git a/Makefile b/Makefile index ef6952ac..265604f2 100644 --- a/Makefile +++ b/Makefile @@ -136,11 +136,11 @@ DOCKER_BUILDER ?= docker buildx DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUILDX_BUILDER_NAME) # tools image build args -TOOLS_ISTIO_VERSION ?= 1.27.1 +TOOLS_ISTIO_VERSION ?= 1.28.1 TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 -TOOLS_KUBECTL_VERSION ?= 1.34.1 +TOOLS_KUBECTL_VERSION ?= 1.34.2 TOOLS_HELM_VERSION ?= 3.19.0 -TOOLS_CILIUM_VERSION ?= 0.18.7 +TOOLS_CILIUM_VERSION ?= 0.18.9 # build args TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION) @@ -239,6 +239,40 @@ report/image-cve: docker-build govulncheck ## Tool Binaries ## Location to install dependencies t +# check-release-version checks if a tool version matches the latest GitHub release +# $1 - variable name (e.g., TOOLS_ISTIO_VERSION) +# $2 - current version value +# $3 - GitHub repo (e.g., istio/istio) +define check-release-version +@LATEST=$$(gh release list --repo $(3) --json tagName,isLatest | jq -r '.[] | select(.isLatest==true) | .tagName'); \ +if [ "$(2)" = "$${LATEST#v}" ]; then \ + echo "✅ $(1)=$(2) == $$LATEST"; \ +else \ + echo "❌ $(1)=$(2) != $$LATEST"; \ +fi +endef + +define check-go-version +@CURRENT_GO=$$(awk '/^go / { print $$2 }' go.mod); \ +LATEST_GO=$$(curl -ks 'https://go.dev/VERSION?m=text' 2>/dev/null | head -1 | sed 's/^go//' || echo "unknown"); \ +if [ "$$CURRENT_GO" = "$$LATEST_GO" ]; then \ + echo "✅ GO_VERSION=$$CURRENT_GO == $$LATEST_GO"; \ +else \ + echo "❌ GO_VERSION=$$CURRENT_GO != $$LATEST_GO"; \ +fi +endef + +.PHONY: check-releases +check-releases: + @echo "Checking tool versions against latest releases..." + @echo "" + $(call check-go-version) + $(call check-release-version,TOOLS_ARGO_ROLLOUTS_VERSION,$(TOOLS_ARGO_ROLLOUTS_VERSION),argoproj/argo-rollouts) + $(call check-release-version,TOOLS_CILIUM_VERSION,$(TOOLS_CILIUM_VERSION),cilium/cilium-cli) + $(call check-release-version,TOOLS_ISTIO_VERSION,$(TOOLS_ISTIO_VERSION),istio/istio) + $(call check-release-version,TOOLS_HELM_VERSION,$(TOOLS_HELM_VERSION),helm/helm) + $(call check-release-version,TOOLS_KUBECTL_VERSION,$(TOOLS_KUBECTL_VERSION),kubernetes/kubernetes) + .PHONY: $(LOCALBIN) $(LOCALBIN): mkdir -p $(LOCALBIN) diff --git a/README.md b/README.md index 0218c692..ae07bae5 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,13 @@ curl -sL https://raw.githubusercontent.com/kagent-dev/tools/refs/heads/main/scri - **Docker:** ```bash -docker run -it --rm -p 8084:8084 ghcr.io/kagent-dev/kagent/tools:0.0.12 +docker run -it --rm -p 8084:8084 ghcr.io/kagent-dev/kagent/tools:0.0.13 ``` - **Kubernetes** ```bash -helm upgrade -i -n kagent --create-namespace kagent-tools oci://ghcr.io/kagent-dev/tools/helm/kagent-tools --version 0.0.12 +helm upgrade -i -n kagent --create-namespace kagent-tools oci://ghcr.io/kagent-dev/tools/helm/kagent-tools --version 0.0.13 helm ls -A ``` diff --git a/go.mod b/go.mod index 0be0049e..9b1a1b0e 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module github.com/kagent-dev/tools -go 1.25.1 +go 1.25.5 require ( github.com/joho/godotenv v1.5.1 - github.com/mark3labs/mcp-go v0.40.0 - github.com/onsi/ginkgo/v2 v2.25.3 + github.com/mark3labs/mcp-go v0.43.2 + github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 - github.com/spf13/cobra v1.10.1 + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 - github.com/tmc/langchaingo v0.1.13 + github.com/tmc/langchaingo v0.1.14 go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 @@ -24,17 +24,15 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/chzyer/readline v1.5.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.10.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 // indirect + github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect - github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect @@ -47,17 +45,18 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.8.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.37.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect - google.golang.org/grpc v1.75.1 // indirect - google.golang.org/protobuf v1.36.9 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.39.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 57006e99..d9dbfe1d 100644 --- a/go.sum +++ b/go.sum @@ -1,483 +1,101 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY= -cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E= -cloud.google.com/go/ai v0.7.0 h1:P6+b5p4gXlza5E+u7uvcgYlzZ7103ACg70YdZeC6oGE= -cloud.google.com/go/ai v0.7.0/go.mod h1:7ozuEcraovh4ABsPbrec3o4LmFl9HigNI3D5haxYeQo= -cloud.google.com/go/aiplatform v1.68.0 h1:EPPqgHDJpBZKRvv+OsB3cr0jYz3EL2pZ+802rBPcG8U= -cloud.google.com/go/aiplatform v1.68.0/go.mod h1:105MFA3svHjC3Oazl7yjXAmIR89LKhRAeNdnDKJczME= -cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw= -cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= -cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= -cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= -cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= -cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= -cloud.google.com/go/vertexai v0.12.0 h1:zTadEo/CtsoyRXNx3uGCncoWAP1H2HakGqwznt+iMo8= -cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/AssemblyAI/assemblyai-go-sdk v1.3.0 h1:AtOVgGxUycvK4P4ypP+1ZupecvFgnfH+Jsum0o5ILoU= -github.com/AssemblyAI/assemblyai-go-sdk v1.3.0/go.mod h1:H0naZbvpIW49cDA5ZZ/gggeXqi7ojSGB1mqshRk6kNE= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Code-Hex/go-generics-cache v1.3.1 h1:i8rLwyhoyhaerr7JpjtYjJZUcCbWOdiYO3fZXLiEC4g= -github.com/Code-Hex/go-generics-cache v1.3.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= -github.com/IBM/watsonx-go v1.0.0 h1:xG7xA2W9N0RsiztR26dwBI8/VxIX4wTBhdYmEis2Yl8= -github.com/IBM/watsonx-go v1.0.0/go.mod h1:8lzvpe/158JkrzvcoIcIj6OdNty5iC9co5nQHfkhRtM= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= -github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= -github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= -github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= -github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/amikos-tech/chroma-go v0.1.2 h1:ECiJ4Gn0AuJaj/jLo+FiqrKRHBVDkrDaUQVRBsEMmEQ= -github.com/amikos-tech/chroma-go v0.1.2/go.mod h1:R/RUp0aaqCWdSXWyIUTfjuNymwqBGLYFgXNZEmisphY= -github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= -github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= -github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= -github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= -github.com/antchfx/xmlquery v1.3.17 h1:d0qWjPp/D+vtRw7ivCwT5ApH/3CkQU8JOeo3245PpTk= -github.com/antchfx/xmlquery v1.3.17/go.mod h1:Afkq4JIeXut75taLSuI31ISJ/zeq+3jG7TunF7noreA= -github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY= -github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= -github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= -github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= -github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= -github.com/aws/aws-sdk-go-v2/config v1.27.12 h1:vq88mBaZI4NGLXk8ierArwSILmYHDJZGJOeAc/pzEVQ= -github.com/aws/aws-sdk-go-v2/config v1.27.12/go.mod h1:IOrsf4IiN68+CgzyuyGUYTpCrtUQTbbMEAtR/MR/4ZU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.12 h1:PVbKQ0KjDosI5+nEdRMU8ygEQDmkJTSHBqPjEX30lqc= -github.com/aws/aws-sdk-go-v2/credentials v1.17.12/go.mod h1:jlWtGFRtKsqc5zqerHZYmKmRkUXo3KPM14YJ13ZEjwE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.1 h1:vTHgBjsGhgKWWIgioxd7MkBH5Ekr8C6Cb+/8iWf1dpc= -github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.1/go.mod h1:nZspkhg+9p8iApLFoyAqfyuMP0F38acy2Hm3r5r95Cg= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.5 h1:Ciiz/plN+Z+pPO1G0W2zJoYIIl0KtKzY0LJ78NXYTws= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.5/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= -github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= -github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= -github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= -github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= -github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= -github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= -github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= -github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= -github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f h1:6jduT9Hfc0njg5jJ1DdKCFPdMBrp/mdZfCpa5h+WM74= -github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= -github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cohere-ai/tokenizer v1.1.2 h1:t3KwUBSpKiBVFtpnHBfVIQNmjfZUuqFVYuSFkZYOWpU= -github.com/cohere-ai/tokenizer v1.1.2/go.mod h1:9MNFPd9j1fuiEK3ua2HSCUxxcrfGMlSqpa93livg/C0= -github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes= -github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= -github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deepmap/oapi-codegen/v2 v2.1.0 h1:I/NMVhJCtuvL9x+S2QzZKpSjGi33oDZwPRdemvOZWyQ= -github.com/deepmap/oapi-codegen/v2 v2.1.0/go.mod h1:R1wL226vc5VmCNJUvMyYr3hJMm5reyv25j952zAVXZ8= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= -github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/gage-technologies/mistral-go v1.1.0 h1:POv1wM9jA/9OBXGV2YdPi9Y/h09+MjCbUF+9hRYlVUI= -github.com/gage-technologies/mistral-go v1.1.0/go.mod h1:tF++Xt7U975GcLlzhrjSQb8l/x+PrriO9QEdsgm9l28= -github.com/getsentry/sentry-go v0.12.0 h1:era7g0re5iY13bHSdN/xMkyV+5zZppjRVQhZrXCaEIk= -github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= -github.com/getzep/zep-go v1.0.4 h1:09o26bPP2RAPKFjWuVWwUWLbtFDF/S8bfbilxzeZAAg= -github.com/getzep/zep-go v1.0.4/go.mod h1:HC1Gz7oiyrzOTvzeKC4dQKUiUy87zpIJl0ZFXXdHuss= -github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= -github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/analysis v0.21.2 h1:hXFrOYFHUAMQdu6zwAiKKJHJQ8kqZs1ux/ru1P1wLJU= -github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/loads v0.21.1 h1:Wb3nVZpdEzDTcly8S4HMkey6fjARRzb7iEaySimlDW0= -github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= -github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= -github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= -github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= -github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= -github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/validate v0.21.0 h1:+Wqk39yKOhfpLqNLEC0/eViCkzM5FVXVqrvt526+wcI= -github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI= -github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= -github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= -github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/generative-ai-go v0.15.1 h1:n8aQUpvhPOlGVuM2DRkJ2jvx04zpp42B778AROJa+pQ= -github.com/google/generative-ai-go v0.15.1/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 h1:ZI8gCoCjGzPsum4L21jHdQs8shFBIQih1TM9Rd/c+EQ= -github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE= +github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= -github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= -github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= -github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= -github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA= -github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= -github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= -github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= -github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= -github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mark3labs/mcp-go v0.40.0 h1:M0oqK412OHBKut9JwXSsj4KanSmEKpzoW8TcxoPOkAU= -github.com/mark3labs/mcp-go v0.40.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/metaphorsystems/metaphor-go v0.0.0-20230816231421-43794c04824e h1:4N462rhrxy7KezYYyL3RjJPWlhXiSkfFes0YsMqicd0= -github.com/metaphorsystems/metaphor-go v0.0.0-20230816231421-43794c04824e/go.mod h1:mDz8kHE7x6Ja95drCQ2T1vLyPRc/t69Cf3wau91E3QU= -github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= -github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= -github.com/milvus-io/milvus-proto/go-api/v2 v2.3.5 h1:4XDy6ATB2Z0fl4Jn0hS6BT6/8YaE0d+ZUf4uBH+Z0Do= -github.com/milvus-io/milvus-proto/go-api/v2 v2.3.5/go.mod h1:1OIl0v5PQeNxIJhCvY+K55CBUOYDZevw9g9380u1Wek= -github.com/milvus-io/milvus-sdk-go/v2 v2.3.6 h1:JVn9OdaronLGmtpxvamQf523mtn3Z/CRxkSZCMWutV4= -github.com/milvus-io/milvus-sdk-go/v2 v2.3.6/go.mod h1:bYFSXVxEj6A/T8BfiR+xkofKbAVZpWiDvKr3SzYUWiA= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= -github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= -github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= -github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= -github.com/nlpodyssey/cybertron v0.2.1 h1:zBvzmjP6Teq3u8yiHuLoUPxan6ZDRq/32GpV6Ep8X08= -github.com/nlpodyssey/cybertron v0.2.1/go.mod h1:Vg9PeB8EkOTAgSKQ68B3hhKUGmB6Vs734dBdCyE4SVM= -github.com/nlpodyssey/gopickle v0.2.0 h1:4naD2DVylYJupQLbCQFdwo6yiXEmPyp+0xf5MVlrBDY= -github.com/nlpodyssey/gopickle v0.2.0/go.mod h1:YIUwjJ2O7+vnBsxUN+MHAAI3N+adqEGiw+nDpwW95bY= -github.com/nlpodyssey/gotokenizers v0.2.0 h1:CWx/sp9s35XMO5lT1kNXCshFGDCfPuuWdx/9JiQBsVc= -github.com/nlpodyssey/gotokenizers v0.2.0/go.mod h1:SBLbuSQhpni9M7U+Ie6O46TXYN73T2Cuw/4eeYHYJ+s= -github.com/nlpodyssey/spago v1.1.0 h1:DGUdGfeGR7TxwkYRdSEzbSvunVWN5heNSksmERmj97w= -github.com/nlpodyssey/spago v1.1.0/go.mod h1:jDWGZwrB4B61U6Tf3/+MVlWOtNsk3EUA7G13UDHlnjQ= -github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= -github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw= -github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= +github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= +github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M= -github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo= -github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= -github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pgvector/pgvector-go v0.1.1 h1:kqJigGctFnlWvskUiYIvJRNwUtQl/aMSUZVs0YWQe+g= -github.com/pgvector/pgvector-go v0.1.1/go.mod h1:wLJgD/ODkdtd2LJK4l6evHXTuG+8PxymYAVomKHOWac= -github.com/pinecone-io/go-pinecone v0.4.1 h1:hRJgtGUIHwvM1NvzKe+YXog4NxYi9x3NdfFhQ2QWBWk= -github.com/pinecone-io/go-pinecone v0.4.1/go.mod h1:KwWSueZFx9zccC+thBk13+LDiOgii8cff9bliUI4tQs= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= -github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo= github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/redis/rueidis v1.0.34 h1:cdggTaDDoqLNeoKMoew8NQY3eTc83Kt6XyfXtoCO2Wc= -github.com/redis/rueidis v1.0.34/go.mod h1:g8nPmgR4C68N3abFiOc/gUOSEKw3Tom6/teYMehg4RE= -github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= -github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= -github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= -github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= -github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= -github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U= -github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI= -github.com/testcontainers/testcontainers-go/modules/chroma v0.31.0 h1:fB/04gfZ9iqm9FO6tEgB8RKU/Dbkc1Opdhp47uiCDSM= -github.com/testcontainers/testcontainers-go/modules/chroma v0.31.0/go.mod h1:dYvKTWVnJ58YizDYX2txYwDG4FvudYUmx37tvbza90o= -github.com/testcontainers/testcontainers-go/modules/milvus v0.31.0 h1:0wTakit4o9Yn0VNkzDOY5hV1LeKcw2W7gxcLa3el2x0= -github.com/testcontainers/testcontainers-go/modules/milvus v0.31.0/go.mod h1:ta9EDZd+lKBMU7enljbNu5H1G495fnT0dw7hmsCPWa0= -github.com/testcontainers/testcontainers-go/modules/mongodb v0.31.0 h1:0ZAEX50NNK/TVRqDls4aQUmokRcYzstKzmF3DCfFK+Y= -github.com/testcontainers/testcontainers-go/modules/mongodb v0.31.0/go.mod h1:n5KbYAdzD8xJrNVGdPvSacJtwZ4D0Q/byTMI5vR/dk8= -github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0 h1:790+S8ewZYCbG+o8IiFlZ8ZZ33XbNO6zV9qhU6xhlRk= -github.com/testcontainers/testcontainers-go/modules/mysql v0.31.0/go.mod h1:REFmO+lSG9S6uSBEwIMZCxeI36uhScjTwChYADeO3JA= -github.com/testcontainers/testcontainers-go/modules/opensearch v0.31.0 h1:sgo2PJb8oCK7ogJjRxAkidXmt+gPzwtyhZpaxSI5wDo= -github.com/testcontainers/testcontainers-go/modules/opensearch v0.31.0/go.mod h1:l4Z7QqGpdk4wTTQk8J8CZ75pfqAz1dizm+LECOLuNVw= -github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 h1:isAwFS3KNKRbJMbWv+wolWqOFUECmjYZ+sIRZCIBc/E= -github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0/go.mod h1:ZNYY8vumNCEG9YI59A9d6/YaMY49uwRhmeU563EzFGw= -github.com/testcontainers/testcontainers-go/modules/qdrant v0.31.0 h1:5bYvi8lSqDnJrO1w5W3AFaSsRe4ZDv4TPj1tsaBEz20= -github.com/testcontainers/testcontainers-go/modules/qdrant v0.31.0/go.mod h1:/3GyFMTSiem1j5mfI/96MufdNvB3A8Xqa+xnV4CUR4A= -github.com/testcontainers/testcontainers-go/modules/redis v0.31.0 h1:5X6GhOdLwV86zcW8sxppJAMtsDC9u+r9tb3biBc9GKs= -github.com/testcontainers/testcontainers-go/modules/redis v0.31.0/go.mod h1:dKi5xBwy1k4u8yb3saQHu7hMEJwewHXxzbcMAuLiA6o= -github.com/testcontainers/testcontainers-go/modules/weaviate v0.31.0 h1:iVJX9O12GHRhqPgIuz/eE8BsNEwyrUMJnWgduBt8quc= -github.com/testcontainers/testcontainers-go/modules/weaviate v0.31.0/go.mod h1:WNc2XhLphiLdNJdjJZvUtRj08ThLY8FL60y7FQSJTPQ= -github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= -github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= -github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= -github.com/weaviate/weaviate v1.24.1 h1:Cl/NnqgFlNfyC7KcjFtETf1bwtTQPLF3oz5vavs+Jq0= -github.com/weaviate/weaviate v1.24.1/go.mod h1:wcg1vJgdIQL5MWBN+871DFJQa+nI2WzyXudmGjJ8cG4= -github.com/weaviate/weaviate-go-client/v4 v4.13.1 h1:7PuK/hpy6Q0b9XaVGiUg5OD1MI/eF2ew9CJge9XdBEE= -github.com/weaviate/weaviate-go-client/v4 v4.13.1/go.mod h1:B2m6g77xWDskrCq1GlU6CdilS0RG2+YXEgzwXRADad0= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc= +github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= -github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= -github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= -github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= -gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 h1:K+bMSIx9A7mLES1rtG+qKduLIXq40DAzYHtb0XuCukA= -gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow= -gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 h1:oYrL81N608MLZhma3ruL8qTM4xcpYECGut8KSxRY59g= -gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8= -gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a h1:O85GKETcmnCNAfv4Aym9tepU8OE0NmcZNqPlXcsBKBs= -gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a/go.mod h1:LaSIs30YPGs1H5jwGgPhLzc8vkNc/k0rDX/fEZqiU/M= -gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84 h1:qqjvoVXdWIcZCLPMlzgA7P9FZWdPGPvP/l3ef8GzV6o= -gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84/go.mod h1:IJZ+fdMvbW2qW6htJx7sLJ04FEs4Ldl/MDsJtMKywfw= -gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f h1:Wku8eEdeJqIOFHtrfkYUByc4bCaTeA6fL0UJgfEiFMI= -gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f/go.mod h1:Tiuhl+njh/JIg0uS/sOJVYi0x2HEa5rc1OAaVsb5tAs= -go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= -go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= -go.mongodb.org/mongo-driver/v2 v2.0.0 h1:Jfd7XpdZa9yk3eY774bO7SWVb30noLSirL9nKTpavhI= -go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -498,69 +116,38 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= -go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg= -go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE= -google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= -google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9 h1:jm6v6kMRpTYKxBRrDkYAitNJegUeO1Mf3Kt80obv0gg= -google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9/go.mod h1:LmwNphe5Afor5V3R5BppOULHOnt2mCIf+NxMd4XiygE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= -nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From ee2121b85e08194a492bf7e22a0035e8e1151fb5 Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Sun, 25 Jan 2026 14:03:53 +0100 Subject: [PATCH 26/41] Tools versions update 2026.1 (#39) * TOOLS_ISTIO_VERSION ?= 1.28.3 TOOLS_KUBECTL_VERSION ?= 1.35.0 TOOLS_HELM_VERSION ?= 4.1.0 TOOLS_CILIUM_VERSION ?= 0.19.0 Signed-off-by: Dmytro Rashko * helm-unittest install --verify=false Signed-off-by: Dmytro Rashko --------- Signed-off-by: Dmytro Rashko --- .devcontainer/devcontainer.json | 8 ++++---- .github/workflows/ci.yaml | 6 +++--- Makefile | 10 +++++----- go.mod | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0d75eec0..f82f308d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,11 +4,11 @@ "dockerfile": "Dockerfile", "args": { "TOOLS_GO_VERSION": "1.25", - "TOOLS_HELM_VERSION": "3.19.0", - "TOOLS_ISTIO_VERSION": "1.27.1", - "TOOLS_KUBECTL_VERSION": "1.34.1", + "TOOLS_ISTIO_VERSION": "1.28.3", "TOOLS_ARGO_ROLLOUTS_VERSION": "1.8.3", - "TOOLS_CILIUM_VERSION": "0.18.7" + "TOOLS_KUBECTL_VERSION": "1.35.0", + "TOOLS_HELM_VERSION": "4.1.0", + "TOOLS_CILIUM_VERSION": "0.19.0" } }, "features": { diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 017cfbac..993c2e55 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,7 +47,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '^1.25.5' + go-version: '^1.25.6' cache: false - name: Run cmd/main.go tests @@ -87,11 +87,11 @@ jobs: - name: Set up Helm uses: azure/setup-helm@v4.2.0 with: - version: v3.17.0 + version: v4.1.0 - name: Install unittest plugin run: | - helm plugin install https://github.com/helm-unittest/helm-unittest + helm plugin install --verify=false https://github.com/helm-unittest/helm-unittest - name: Chart init run: | diff --git a/Makefile b/Makefile index 265604f2..e2ed9b0b 100644 --- a/Makefile +++ b/Makefile @@ -136,11 +136,11 @@ DOCKER_BUILDER ?= docker buildx DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUILDX_BUILDER_NAME) # tools image build args -TOOLS_ISTIO_VERSION ?= 1.28.1 +TOOLS_ISTIO_VERSION ?= 1.28.3 TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 -TOOLS_KUBECTL_VERSION ?= 1.34.2 -TOOLS_HELM_VERSION ?= 3.19.0 -TOOLS_CILIUM_VERSION ?= 0.18.9 +TOOLS_KUBECTL_VERSION ?= 1.35.0 +TOOLS_HELM_VERSION ?= 4.1.0 +TOOLS_CILIUM_VERSION ?= 0.19.0 # build args TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION) @@ -196,7 +196,7 @@ helm-publish: helm-version .PHONY: helm-test helm-test: helm-version mkdir -p tmp - helm plugin ls | grep unittest || helm plugin install https://github.com/helm-unittest/helm-unittest.git + helm plugin ls | grep unittest || helm plugin install --verify=false https://github.com/helm-unittest/helm-unittest.git helm unittest helm/kagent-tools .PHONY: create-kind-cluster diff --git a/go.mod b/go.mod index 9b1a1b0e..a4df5e26 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/kagent-dev/tools -go 1.25.5 +go 1.25.6 require ( github.com/joho/godotenv v1.5.1 From 0b4e7ab771c78c292c132e80163bd399c11bc880 Mon Sep 17 00:00:00 2001 From: Ben Hirschberg <59160382+slashben@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:30:37 +0200 Subject: [PATCH 27/41] Kubescape tool support (#38) * Add Kubescape integration - Introduced Kubescape tool support, including registration of various tools for health checks, vulnerability manifests, and configuration scans. - Implemented specific error handling for Kubescape-related operations, providing detailed suggestions based on error types. Signed-off-by: Ben * Enhance Kubescape tool by adding runtime observability features - Introduced checks for ApplicationProfiles and NetworkNeighborhoods CRDs in health checks, with corresponding recommendations for enabling runtime observability. - Added handlers for listing and retrieving ApplicationProfiles and NetworkNeighborhoods, capturing runtime behavior and network communication patterns of workloads. Signed-off-by: Ben * Fix linter errors: remove unused SBOM functions and suppress deprecated test warnings Signed-off-by: Ben * ci: increase golangci-lint timeout to 5m to prevent context deadline errors Signed-off-by: Ben * Updating timeouts for golint Signed-off-by: Ben --------- Signed-off-by: Ben --- Makefile | 4 +- cmd/main.go | 2 + go.mod | 137 +++- go.sum | 1178 +++++++++++++++++++++++++++++- internal/errors/tool_errors.go | 71 ++ pkg/kubescape/kubescape.go | 1206 +++++++++++++++++++++++++++++++ pkg/kubescape/kubescape_test.go | 1193 ++++++++++++++++++++++++++++++ pkg/kubescape/testing.go | 28 + 8 files changed, 3808 insertions(+), 11 deletions(-) create mode 100644 pkg/kubescape/kubescape.go create mode 100644 pkg/kubescape/kubescape_test.go create mode 100644 pkg/kubescape/testing.go diff --git a/Makefile b/Makefile index e2ed9b0b..892c7a17 100644 --- a/Makefile +++ b/Makefile @@ -37,11 +37,11 @@ vet: ## Run go vet against code. .PHONY: lint lint: golangci-lint ## Run golangci-lint linter - $(GOLANGCI_LINT) run --build-tags=test + $(GOLANGCI_LINT) run --build-tags=test --timeout=10m .PHONY: lint-fix lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes - $(GOLANGCI_LINT) run --build-tags=test --fix + $(GOLANGCI_LINT) run --build-tags=test --fix --timeout=10m .PHONY: lint-config lint-config: golangci-lint ## Verify golangci-lint linter configuration diff --git a/cmd/main.go b/cmd/main.go index fa737dd6..c1189e1d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,6 +22,7 @@ import ( "github.com/kagent-dev/tools/pkg/helm" "github.com/kagent-dev/tools/pkg/istio" "github.com/kagent-dev/tools/pkg/k8s" + "github.com/kagent-dev/tools/pkg/kubescape" "github.com/kagent-dev/tools/pkg/prometheus" "github.com/kagent-dev/tools/pkg/utils" "github.com/spf13/cobra" @@ -292,6 +293,7 @@ func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfi "helm": helm.RegisterTools, "istio": istio.RegisterTools, "k8s": func(s *server.MCPServer) { k8s.RegisterTools(s, nil, kubeconfig) }, + "kubescape": func(s *server.MCPServer) { kubescape.RegisterTools(s, kubeconfig) }, "prometheus": prometheus.RegisterTools, "utils": utils.RegisterTools, } diff --git a/go.mod b/go.mod index a4df5e26..3f7722c6 100644 --- a/go.mod +++ b/go.mod @@ -4,59 +4,190 @@ go 1.25.6 require ( github.com/joho/godotenv v1.5.1 + github.com/kubescape/k8s-interface v0.0.200 + github.com/kubescape/storage v0.0.238 github.com/mark3labs/mcp-go v0.43.2 github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/tmc/langchaingo v0.1.14 - go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel v1.39.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 - go.opentelemetry.io/otel/metric v1.38.0 + go.opentelemetry.io/otel/metric v1.39.0 go.opentelemetry.io/otel/sdk v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 + go.opentelemetry.io/otel/trace v1.39.0 + k8s.io/api v0.35.0 + k8s.io/apiextensions-apiserver v0.35.0 + k8s.io/apimachinery v0.35.0 + k8s.io/client-go v0.35.0 ) require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/acobaugh/osrelease v0.1.0 // indirect + github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 // indirect + github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 // indirect + github.com/anchore/stereoscope v0.1.9-0.20250826202322-ef061ea78385 // indirect + github.com/anchore/syft v1.32.0 // indirect + github.com/armosec/armoapi-go v0.0.596 // indirect + github.com/armosec/gojay v1.2.17 // indirect + github.com/armosec/utils-go v0.0.58 // indirect + github.com/armosec/utils-k8s-go v0.0.30 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/becheran/wildmatch-go v1.0.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/briandowns/spinner v1.23.2 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cilium/cilium v1.16.17 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containers/common v0.63.0 // indirect + github.com/coreos/go-oidc/v3 v3.14.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/docker/cli v28.3.2+incompatible // indirect + github.com/docker/docker v28.3.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/facebookincubator/nvdtools v0.1.5 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/github/go-spdx/v2 v2.3.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.23.0 // indirect + github.com/go-openapi/errors v0.22.1 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/loads v0.22.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/validate v0.24.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/gohugoio/hashstructure v0.5.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.20.6 // indirect + github.com/google/licensecheck v0.3.1 // indirect github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect + github.com/jinzhu/copier v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/kubescape/go-logger v0.0.26 // indirect + github.com/mackerelio/go-osstat v0.2.5 // indirect github.com/mailru/easyjson v0.9.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/olvrng/ujson v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opencontainers/runtime-spec v1.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.8 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sasha-s/go-deadlock v0.3.5 // indirect + github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect + github.com/seccomp/libseccomp-golang v0.10.0 // indirect + github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.20.1 // indirect + github.com/stripe/stripe-go/v74 v74.30.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/sylabs/squashfs v1.0.6 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect + github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2 // indirect + github.com/uptrace/uptrace-go v1.37.0 // indirect + github.com/vishvananda/netlink v1.3.1-0.20241022031324-976bd8de7d81 // indirect + github.com/vishvananda/netns v0.0.5 // indirect + github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect + github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/yl2chen/cidranger v1.0.2 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + go.mongodb.org/mongo-driver v1.17.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 // indirect + go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/log v0.15.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.8.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.39.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiserver v0.35.0 // indirect + k8s.io/component-base v0.35.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/controller-runtime v0.20.4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index d9dbfe1d..58327a02 100644 --- a/go.sum +++ b/go.sum @@ -1,85 +1,686 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= +github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716 h1:2sIdYJlQESEnyk3Y0WD2vXWW5eD2iMz9Ev8fj1Z8LNA= +github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716/go.mod h1:Utb9i4kwiCWvqAIxZaJeMIXFO9uOgQXlvH2BfbfO/zI= +github.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe h1:qv/xxpjF5RdKPqZjx8RM0aBi3HUCAO0DhRBMs2xhY1I= +github.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe/go.mod h1:vrcYMDps9YXwwx2a9AsvipM6Fi5H9//9bymGb8G8BIQ= +github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc= +github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50= +github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM= +github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw= +github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY= +github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI= +github.com/anchore/stereoscope v0.1.9-0.20250826202322-ef061ea78385 h1:icCqbvAKGZXf29lEi8JmwvHVCBCYkiyZMuSnk+5ajYo= +github.com/anchore/stereoscope v0.1.9-0.20250826202322-ef061ea78385/go.mod h1:0UCjLz5MdPNiH9F0h2tSNf3yGF6/MnK8ZCPo0YfDQVc= +github.com/anchore/syft v1.32.0 h1:JcX9W+P/Xjv5DNg3TNBtwiEyZommuTaP16/NC9r0Yfo= +github.com/anchore/syft v1.32.0/go.mod h1:E6Kd4iBM2ljUOUQvSt7hVK6vBwaHkMXwcvBZmGMSY5o= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armosec/armoapi-go v0.0.596 h1:n8xB6Y/zuzjAqqwc7zJPXxdvn6pqZK94IC6x7nvj1oI= +github.com/armosec/armoapi-go v0.0.596/go.mod h1:GQQzRuP8OBvbDx7GGwOyw3TCjk5NtK3WbeyfuLoiEts= +github.com/armosec/gojay v1.2.17 h1:VSkLBQzD1c2V+FMtlGFKqWXNsdNvIKygTKJI9ysY8eM= +github.com/armosec/gojay v1.2.17/go.mod h1:vuvX3DlY0nbVrJ0qCklSS733AWMoQboq3cFyuQW9ybc= +github.com/armosec/utils-go v0.0.58 h1:g9RnRkxZAmzTfPe2ruMo2OXSYLwVSegQSkSavOfmaIE= +github.com/armosec/utils-go v0.0.58/go.mod h1:CdqKHKruVJMCxGcZXYW9J+5P9FZou8dMzVpcB0Xt8pk= +github.com/armosec/utils-k8s-go v0.0.30 h1:Gj8MJck0jZPSLSq8ZMiRPT3F/laOYQdaLxXKKcjijt4= +github.com/armosec/utils-k8s-go v0.0.30/go.mod h1:t0vvPJhYE+X+bOsaMsD2SzWU7WkJmV2Ltn9hg66AIe8= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= +github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= +github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/cilium v1.16.17 h1:5DUFyl/DhCEhWGdMQjw0eA9FlI8dBFpg2ENYJan3Wk0= +github.com/cilium/cilium v1.16.17/go.mod h1:Wa47utg/8XuOe8pq64KwNOM8wDsXoYP3HZ6uw3IYDVg= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containers/common v0.63.0 h1:ox6vgUYX5TSvt4W+bE36sYBVz/aXMAfRGVAgvknSjBg= +github.com/containers/common v0.63.0/go.mod h1:+3GCotSqNdIqM3sPs152VvW7m5+Mg8Kk+PExT3G9hZw= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY= +github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk= +github.com/facebookincubator/nvdtools v0.1.5 h1:jbmDT1nd6+k+rlvKhnkgMokrCAzHoASWE5LtHbX2qFQ= +github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrEKH4iB0OJby3N8GRJO4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= +github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/github/go-spdx/v2 v2.3.3 h1:QI7evnHWEfWkT54eJwkoV/f3a0xD3gLlnVmT5wQG6LE= +github.com/github/go-spdx/v2 v2.3.3/go.mod h1:2ZxKsOhvBp+OYBDlsGnUMcchLeo2mrpEBn2L1C+U3IQ= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= +github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg= +github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= +github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs= +github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE= github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kubescape/go-logger v0.0.26 h1:pOADnCLaooXwa7XUzmelCasq8h/vWhjHScJITuKoTUk= +github.com/kubescape/go-logger v0.0.26/go.mod h1:qfUg4BGH2Rbxy+Tn3g4ks2Lt7zOENKA8AMXnAfkZpl0= +github.com/kubescape/k8s-interface v0.0.200 h1:Ff64dlDigg8dDYJuaeLFFjfTCHQNC1SStWNECWFRCYE= +github.com/kubescape/k8s-interface v0.0.200/go.mod h1:j9snZbH+RxOaa1yG/bWgTClj90q7To0rGgQepxy4b+k= +github.com/kubescape/storage v0.0.238 h1:4PNM/6RZSNTgLddHNsSId/58CpZQw8uHQ0KhOakHU0Y= +github.com/kubescape/storage v0.0.238/go.mod h1:TGlwvK5ixF6Zfm/qyeRw10D0DyN43QFU5VUZmYfm80k= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o= +github.com/mackerelio/go-osstat v0.2.5/go.mod h1:atxwWF+POUZcdtR1wnsUcQxTytoHG4uhl2AKKzrOajY= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olvrng/ujson v1.1.0 h1:8xVUzVlqwdMVWh5d1UHBtLQ1D50nxoPuPEq9Wozs8oA= +github.com/olvrng/ujson v1.1.0/go.mod h1:Mz4G3RODTUfbkKyvi0lgmPx/7vd3Saksk+1jgk8s9xo= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= +github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.9.1-0.20250303011046-260e151b8552 h1:CkXngT0nixZqQUPDVfwVs3GiuhfTqCMk0V+OoHpxIvA= +github.com/opencontainers/runtime-tools v0.9.1-0.20250303011046-260e151b8552/go.mod h1:T487Kf80NeF2i0OyVXHiylg217e0buz8pQsa0T791RA= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= +github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 h1:ah1dvbqPMN5+ocrg/ZSgZ6k8bOk+kcZQ7fnyx6UvOm4= +github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo= github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= +github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= +github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seccomp/libseccomp-golang v0.10.0 h1:aA4bp+/Zzi0BnWZ2F1wgNBs5gTpm+na2rWM6M9YjLpY= +github.com/seccomp/libseccomp-golang v0.10.0/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= +github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= +github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI= +github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -90,14 +691,66 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc= github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W2ryNgg8x8rVeKVA20KJUkfS0= +github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= +github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2 h1:cj/Z6FKTTYBnstI0Lni9PA+k2foounKIPUmj1LBwNiQ= +github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2/go.mod h1:LDaXk90gKEC2nC7JH3Lpnhfu+2V7o/TsqomJJmqA39o= +github.com/uptrace/uptrace-go v1.37.0 h1:9ohbWB0qZEfcPLFbfqAAt5wz2rcBmL60/QqkOkvqYOs= +github.com/uptrace/uptrace-go v1.37.0/go.mod h1:3xAdXLVyEoqvRwuj3D/n1s9bLl7Ok+OnNaW889fvtDQ= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/vishvananda/netlink v1.3.1-0.20241022031324-976bd8de7d81 h1:9fkQcQYvtTr9ayFXuMfDMVuDt4+BYG9FwsGLnrBde0M= +github.com/vishvananda/netlink v1.3.1-0.20241022031324-976bd8de7d81/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= +github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20= +github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 h1:0KGbf+0SMg+UFy4e1A/CPVvXn21f1qtWdeJwxZFoQG8= +github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= +github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= +go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM= +go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc= +go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 h1:ZIt0ya9/y4WyRIzfLC8hQRRsWg0J9M9GyaGtIMiElZI= +go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0/go.mod h1:F1aJ9VuiKWOlWwKdTYDUp1aoS0HzQxg38/VLxKmhm5U= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 h1:zUfYw8cscHHLwaY8Xz3fiJu+R59xBnkgq2Zr1lwmK/0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0/go.mod h1:514JLMCcFLQFS8cnTepOk6I09cKWJ5nGHBxHrMJ8Yfg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0 h1:9PgnL3QNlj10uGxExowIDIZu66aVBwWhXmbOp1pa6RA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0/go.mod h1:0ineDcLELf6JmKfuo0wvvhAVMuxWFYvkTin2iV4ydPQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= @@ -106,48 +759,561 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= +go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ= +go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw= +go.opentelemetry.io/otel/sdk/log/logtest v0.13.0 h1:9yio6AFZ3QD9j9oqshV1Ibm9gPLlHNxurno5BreMtIA= +go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= +k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= +k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= +k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= +k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/internal/errors/tool_errors.go b/internal/errors/tool_errors.go index 26771647..12a7fd9c 100644 --- a/internal/errors/tool_errors.go +++ b/internal/errors/tool_errors.go @@ -298,6 +298,77 @@ func NewCiliumError(operation string, cause error) *ToolError { return err } +// NewKubescapeError creates a Kubescape-specific error +func NewKubescapeError(operation string, cause error) *ToolError { + err := NewToolError("Kubescape", operation, cause) + + causeStr := cause.Error() + if strings.Contains(causeStr, "not found") { + // Determine which capability might be missing based on the operation + suggestions := []string{ + "Check if the Kubescape operator is installed in the cluster", + "Verify the resource name and namespace", + } + if strings.Contains(operation, "vulnerabilit") || strings.Contains(operation, "sbom") { + suggestions = append(suggestions, + "Ensure vulnerability scanning is enabled in the Kubescape Helm chart", + "Enable with: helm upgrade kubescape kubescape/kubescape-operator -n kubescape --set capabilities.vulnerabilityScan=enable", + "Use 'kubectl get vulnerabilitymanifests -A' to list available manifests", + ) + } else if strings.Contains(operation, "configuration") { + suggestions = append(suggestions, + "Ensure configuration scanning is enabled in the Kubescape Helm chart", + "Enable with: helm upgrade kubescape kubescape/kubescape-operator -n kubescape --set capabilities.continuousScan=enable", + "Use 'kubectl get workloadconfigurationscans -A' to list available scans", + ) + } else if strings.Contains(operation, "application_profile") || strings.Contains(operation, "network_neighborhood") { + suggestions = append(suggestions, + "Ensure runtime observability is enabled in the Kubescape Helm chart", + "Enable with: helm upgrade kubescape kubescape/kubescape-operator -n kubescape --set capabilities.runtimeObservability=enable", + "Runtime data collection requires time - allow workloads to run before profiles are available", + ) + if strings.Contains(operation, "application_profile") { + suggestions = append(suggestions, "Use 'kubectl get applicationprofiles -A' to list available profiles") + } else { + suggestions = append(suggestions, "Use 'kubectl get networkneighborhoods -A' to list available network data") + } + } else { + suggestions = append(suggestions, + "Ensure the required scanning capabilities are enabled in the Kubescape Helm chart", + "For vulnerability scanning: --set capabilities.vulnerabilityScan=enable", + "For configuration scanning: --set capabilities.continuousScan=enable", + "For runtime observability: --set capabilities.runtimeObservability=enable", + ) + } + err = err.WithSuggestions(suggestions...).WithRetryable(false).WithErrorCode("KUBESCAPE_RESOURCE_NOT_FOUND") + } else if strings.Contains(causeStr, "connection refused") || strings.Contains(causeStr, "timeout") { + err = err.WithSuggestions( + "Check if the Kubernetes cluster is accessible", + "Verify your kubeconfig is correct", + "Ensure network connectivity to the cluster", + ).WithRetryable(true).WithErrorCode("KUBESCAPE_CONNECTION_ERROR") + } else if strings.Contains(causeStr, "forbidden") { + err = err.WithSuggestions( + "Check your RBAC permissions for Kubescape CRDs", + "Verify your service account has read access to Kubescape storage CRDs", + "Required CRDs: VulnerabilityManifests, WorkloadConfigurationScans, ApplicationProfiles, NetworkNeighborhoods, SBOMSyfts", + "Contact your cluster administrator", + ).WithRetryable(false).WithErrorCode("KUBESCAPE_PERMISSION_ERROR") + } else { + err = err.WithSuggestions( + "Check Kubescape operator status: kubectl get pods -n kubescape", + "Verify kubeconfig is valid", + "Check if CRDs are installed: kubectl get crd vulnerabilitymanifests.spdx.softwarecomposition.kubescape.io", + "Ensure scanning capabilities are enabled in the Helm chart:", + " - Vulnerability scanning: --set capabilities.vulnerabilityScan=enable", + " - Configuration scanning: --set capabilities.continuousScan=enable", + " - Runtime observability: --set capabilities.runtimeObservability=enable", + ).WithRetryable(true).WithErrorCode("KUBESCAPE_GENERIC_ERROR") + } + + return err +} + // NewValidationError creates a validation error func NewValidationError(field, message string) *ToolError { err := NewToolError("Validation", fmt.Sprintf("validate %s", field), fmt.Errorf("%s", message)) diff --git a/pkg/kubescape/kubescape.go b/pkg/kubescape/kubescape.go new file mode 100644 index 00000000..8893da30 --- /dev/null +++ b/pkg/kubescape/kubescape.go @@ -0,0 +1,1206 @@ +package kubescape + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/kagent-dev/tools/internal/errors" + "github.com/kagent-dev/tools/internal/telemetry" + helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + spdxv1beta1 "github.com/kubescape/storage/pkg/generated/clientset/versioned/typed/softwarecomposition/v1beta1" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + corev1 "k8s.io/api/core/v1" + apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +const ( + defaultKubescapeNamespace = "kubescape" + + // CRD names + vulnerabilityManifestsCRD = "vulnerabilitymanifests.spdx.softwarecomposition.kubescape.io" + workloadConfigurationScansCRD = "workloadconfigurationscans.spdx.softwarecomposition.kubescape.io" + applicationProfilesCRD = "applicationprofiles.spdx.softwarecomposition.kubescape.io" + networkNeighborhoodsCRD = "networkneighborhoods.spdx.softwarecomposition.kubescape.io" + sbomSyftsCRD = "sbomsyfts.spdx.softwarecomposition.kubescape.io" + + // Pod labels + operatorPodLabel = "app.kubernetes.io/name=kubescape-operator" + storagePodLabel = "app.kubernetes.io/name=storage" +) + +// KubescapeTool holds the clients for Kubescape and Kubernetes APIs +type KubescapeTool struct { + spdxClient spdxv1beta1.SpdxV1beta1Interface + k8sClient kubernetes.Interface + apiExtClient apiextensionsclientset.Interface + initError error +} + +// NewKubescapeTool creates a new KubescapeTool with Kubernetes clients +func NewKubescapeTool(kubeconfig string) *KubescapeTool { + tool := &KubescapeTool{} + + config, err := getKubeConfig(kubeconfig) + if err != nil { + tool.initError = fmt.Errorf("failed to create kubernetes config: %w", err) + return tool + } + + // Create standard Kubernetes client + k8sClient, err := kubernetes.NewForConfig(config) + if err != nil { + tool.initError = fmt.Errorf("failed to create kubernetes client: %w", err) + return tool + } + tool.k8sClient = k8sClient + + // Create API extensions client for CRD checks + apiExtClient, err := apiextensionsclientset.NewForConfig(config) + if err != nil { + tool.initError = fmt.Errorf("failed to create apiextensions client: %w", err) + return tool + } + tool.apiExtClient = apiExtClient + + // Create Kubescape storage client + spdxClient, err := spdxv1beta1.NewForConfig(config) + if err != nil { + tool.initError = fmt.Errorf("failed to create kubescape client: %w", err) + return tool + } + tool.spdxClient = spdxClient + + return tool +} + +func getKubeConfig(kubeconfig string) (*rest.Config, error) { + if kubeconfig != "" { + return clientcmd.BuildConfigFromFlags("", kubeconfig) + } + // Try in-cluster config first, then fall back to default kubeconfig location + config, err := rest.InClusterConfig() + if err != nil { + // Fall back to default kubeconfig + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + configOverrides := &clientcmd.ConfigOverrides{} + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) + return kubeConfig.ClientConfig() + } + return config, nil +} + +// HealthCheckResult represents the result of a health check +type HealthCheckResult struct { + Healthy bool `json:"healthy"` + Checks map[string]CheckStatus `json:"checks"` + Summary string `json:"summary"` + Recommendations []string `json:"recommendations,omitempty"` +} + +// CheckStatus represents the status of a single check +type CheckStatus struct { + Status string `json:"status"` + Message string `json:"message"` + Details interface{} `json:"details,omitempty"` +} + +// handleCheckHealth verifies Kubescape operator installation and readiness +func (k *KubescapeTool) handleCheckHealth(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if k.initError != nil { + toolErr := errors.NewKubescapeError("check_health", k.initError) + return toolErr.ToMCPResult(), nil + } + + namespace := mcp.ParseString(request, "namespace", defaultKubescapeNamespace) + + result := HealthCheckResult{ + Healthy: true, + Checks: make(map[string]CheckStatus), + } + var recommendations []string + + // Check 1: Namespace exists + _, err := k.k8sClient.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + result.Checks["namespace"] = CheckStatus{ + Status: "error", + Message: fmt.Sprintf("Namespace '%s' not found", namespace), + } + result.Healthy = false + recommendations = append(recommendations, fmt.Sprintf("Create the namespace: kubectl create namespace %s", namespace)) + } else { + result.Checks["namespace"] = CheckStatus{ + Status: "error", + Message: fmt.Sprintf("Failed to check namespace: %v", err), + } + result.Healthy = false + } + } else { + result.Checks["namespace"] = CheckStatus{ + Status: "ok", + Message: fmt.Sprintf("Namespace '%s' exists", namespace), + } + } + + // Check 2: Operator pods running + operatorPods, err := k.k8sClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: operatorPodLabel, + }) + if err != nil { + result.Checks["operator_pods"] = CheckStatus{ + Status: "error", + Message: fmt.Sprintf("Failed to list operator pods: %v", err), + } + result.Healthy = false + } else if len(operatorPods.Items) == 0 { + result.Checks["operator_pods"] = CheckStatus{ + Status: "error", + Message: "No operator pods found", + } + result.Healthy = false + recommendations = append(recommendations, "Install Kubescape operator: helm upgrade --install kubescape kubescape/kubescape-operator -n kubescape --create-namespace") + } else { + runningCount := 0 + podDetails := []map[string]string{} + for _, pod := range operatorPods.Items { + status := string(pod.Status.Phase) + if pod.Status.Phase == corev1.PodRunning { + runningCount++ + } + podDetails = append(podDetails, map[string]string{ + "name": pod.Name, + "status": status, + }) + } + if runningCount == len(operatorPods.Items) { + result.Checks["operator_pods"] = CheckStatus{ + Status: "ok", + Message: fmt.Sprintf("%d/%d pods running", runningCount, len(operatorPods.Items)), + Details: podDetails, + } + } else { + result.Checks["operator_pods"] = CheckStatus{ + Status: "warning", + Message: fmt.Sprintf("%d/%d pods running", runningCount, len(operatorPods.Items)), + Details: podDetails, + } + result.Healthy = false + recommendations = append(recommendations, fmt.Sprintf("Check operator logs: kubectl logs -n %s -l %s", namespace, operatorPodLabel)) + } + } + + // Check 3: Storage pods running + storagePods, err := k.k8sClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: storagePodLabel, + }) + if err != nil { + result.Checks["storage_pods"] = CheckStatus{ + Status: "error", + Message: fmt.Sprintf("Failed to list storage pods: %v", err), + } + result.Healthy = false + } else if len(storagePods.Items) == 0 { + result.Checks["storage_pods"] = CheckStatus{ + Status: "warning", + Message: "No storage pods found (may be using external storage)", + } + } else { + runningCount := 0 + for _, pod := range storagePods.Items { + if pod.Status.Phase == corev1.PodRunning { + runningCount++ + } + } + if runningCount == len(storagePods.Items) { + result.Checks["storage_pods"] = CheckStatus{ + Status: "ok", + Message: fmt.Sprintf("%d/%d pods running", runningCount, len(storagePods.Items)), + } + } else { + result.Checks["storage_pods"] = CheckStatus{ + Status: "warning", + Message: fmt.Sprintf("%d/%d pods running", runningCount, len(storagePods.Items)), + } + } + } + + // Check 4: VulnerabilityManifests CRD exists + _, err = k.apiExtClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, vulnerabilityManifestsCRD, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + result.Checks["vulnerability_crd"] = CheckStatus{ + Status: "error", + Message: "VulnerabilityManifests CRD not installed - vulnerability scanning may not be enabled", + } + result.Healthy = false + recommendations = append(recommendations, + "Enable vulnerability scanning in Kubescape Helm chart: helm upgrade --install kubescape kubescape/kubescape-operator -n kubescape --set capabilities.vulnerabilityScan=enable") + } else { + result.Checks["vulnerability_crd"] = CheckStatus{ + Status: "error", + Message: fmt.Sprintf("Failed to check CRD: %v", err), + } + result.Healthy = false + } + } else { + result.Checks["vulnerability_crd"] = CheckStatus{ + Status: "ok", + Message: "CRD installed", + } + } + + // Check 5: WorkloadConfigurationScans CRD exists + _, err = k.apiExtClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, workloadConfigurationScansCRD, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + result.Checks["configuration_crd"] = CheckStatus{ + Status: "error", + Message: "WorkloadConfigurationScans CRD not installed - configuration scanning may not be enabled", + } + result.Healthy = false + recommendations = append(recommendations, + "Enable configuration scanning in Kubescape Helm chart: helm upgrade --install kubescape kubescape/kubescape-operator -n kubescape --set capabilities.continuousScan=enable") + } else { + result.Checks["configuration_crd"] = CheckStatus{ + Status: "error", + Message: fmt.Sprintf("Failed to check CRD: %v", err), + } + result.Healthy = false + } + } else { + result.Checks["configuration_crd"] = CheckStatus{ + Status: "ok", + Message: "CRD installed", + } + } + + // Check 6: Vulnerability scan data available + manifests, err := k.spdxClient.VulnerabilityManifests(metav1.NamespaceAll).List(ctx, metav1.ListOptions{Limit: 1}) + if err != nil { + result.Checks["vulnerability_scan_data"] = CheckStatus{ + Status: "warning", + Message: fmt.Sprintf("Failed to list vulnerability manifests: %v", err), + } + } else if len(manifests.Items) == 0 { + result.Checks["vulnerability_scan_data"] = CheckStatus{ + Status: "warning", + Message: "No vulnerability manifests found - scans may not have completed yet or vulnerability scanning may be disabled", + } + recommendations = append(recommendations, + "If vulnerability scanning is not working, ensure it is enabled: helm upgrade kubescape kubescape/kubescape-operator -n kubescape --set capabilities.vulnerabilityScan=enable") + } else { + // Get actual count + allManifests, _ := k.spdxClient.VulnerabilityManifests(metav1.NamespaceAll).List(ctx, metav1.ListOptions{}) + count := 0 + if allManifests != nil { + count = len(allManifests.Items) + } + result.Checks["vulnerability_scan_data"] = CheckStatus{ + Status: "ok", + Message: fmt.Sprintf("%d vulnerability manifests found", count), + } + } + + // Check 7: Configuration scan data available + configScans, err := k.spdxClient.WorkloadConfigurationScans(metav1.NamespaceAll).List(ctx, metav1.ListOptions{Limit: 1}) + if err != nil { + result.Checks["configuration_scan_data"] = CheckStatus{ + Status: "warning", + Message: fmt.Sprintf("Failed to list configuration scans: %v", err), + } + } else if len(configScans.Items) == 0 { + result.Checks["configuration_scan_data"] = CheckStatus{ + Status: "warning", + Message: "No configuration scans found - scans may not have completed yet or continuous scanning may be disabled", + } + recommendations = append(recommendations, + "If configuration scanning is not working, ensure it is enabled: helm upgrade kubescape kubescape/kubescape-operator -n kubescape --set capabilities.continuousScan=enable") + } else { + // Get actual count + allConfigScans, _ := k.spdxClient.WorkloadConfigurationScans(metav1.NamespaceAll).List(ctx, metav1.ListOptions{}) + count := 0 + if allConfigScans != nil { + count = len(allConfigScans.Items) + } + result.Checks["configuration_scan_data"] = CheckStatus{ + Status: "ok", + Message: fmt.Sprintf("%d configuration scans found", count), + } + } + + // Check 8: ApplicationProfiles CRD exists (runtime observability) + _, err = k.apiExtClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, applicationProfilesCRD, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + result.Checks["application_profiles_crd"] = CheckStatus{ + Status: "warning", + Message: "ApplicationProfiles CRD not installed - runtime observability may not be enabled", + } + recommendations = append(recommendations, + "Enable runtime observability for workload behavior analysis: helm upgrade kubescape kubescape/kubescape-operator -n kubescape --set capabilities.runtimeObservability=enable") + } else { + result.Checks["application_profiles_crd"] = CheckStatus{ + Status: "error", + Message: fmt.Sprintf("Failed to check CRD: %v", err), + } + } + } else { + result.Checks["application_profiles_crd"] = CheckStatus{ + Status: "ok", + Message: "CRD installed", + } + + // Check for ApplicationProfile data + profiles, listErr := k.spdxClient.ApplicationProfiles(metav1.NamespaceAll).List(ctx, metav1.ListOptions{Limit: 1}) + if listErr != nil { + result.Checks["application_profiles_data"] = CheckStatus{ + Status: "warning", + Message: fmt.Sprintf("Failed to list application profiles: %v", listErr), + } + } else if len(profiles.Items) == 0 { + result.Checks["application_profiles_data"] = CheckStatus{ + Status: "warning", + Message: "No application profiles found - runtime learning may not have completed yet", + } + } else { + allProfiles, _ := k.spdxClient.ApplicationProfiles(metav1.NamespaceAll).List(ctx, metav1.ListOptions{}) + count := 0 + if allProfiles != nil { + count = len(allProfiles.Items) + } + result.Checks["application_profiles_data"] = CheckStatus{ + Status: "ok", + Message: fmt.Sprintf("%d application profiles found", count), + } + } + } + + // Check 9: NetworkNeighborhoods CRD exists (runtime observability) + _, err = k.apiExtClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, networkNeighborhoodsCRD, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + result.Checks["network_neighborhoods_crd"] = CheckStatus{ + Status: "warning", + Message: "NetworkNeighborhoods CRD not installed - runtime observability may not be enabled", + } + // Only add recommendation if not already added from ApplicationProfiles check + hasRuntimeRecommendation := false + for _, r := range recommendations { + if strings.Contains(r, "runtimeObservability") { + hasRuntimeRecommendation = true + break + } + } + if !hasRuntimeRecommendation { + recommendations = append(recommendations, + "Enable runtime observability for network analysis: helm upgrade kubescape kubescape/kubescape-operator -n kubescape --set capabilities.runtimeObservability=enable") + } + } else { + result.Checks["network_neighborhoods_crd"] = CheckStatus{ + Status: "error", + Message: fmt.Sprintf("Failed to check CRD: %v", err), + } + } + } else { + result.Checks["network_neighborhoods_crd"] = CheckStatus{ + Status: "ok", + Message: "CRD installed", + } + + // Check for NetworkNeighborhood data + neighborhoods, listErr := k.spdxClient.NetworkNeighborhoods(metav1.NamespaceAll).List(ctx, metav1.ListOptions{Limit: 1}) + if listErr != nil { + result.Checks["network_neighborhoods_data"] = CheckStatus{ + Status: "warning", + Message: fmt.Sprintf("Failed to list network neighborhoods: %v", listErr), + } + } else if len(neighborhoods.Items) == 0 { + result.Checks["network_neighborhoods_data"] = CheckStatus{ + Status: "warning", + Message: "No network neighborhoods found - runtime learning may not have completed yet", + } + } else { + allNeighborhoods, _ := k.spdxClient.NetworkNeighborhoods(metav1.NamespaceAll).List(ctx, metav1.ListOptions{}) + count := 0 + if allNeighborhoods != nil { + count = len(allNeighborhoods.Items) + } + result.Checks["network_neighborhoods_data"] = CheckStatus{ + Status: "ok", + Message: fmt.Sprintf("%d network neighborhoods found", count), + } + } + } + + // NOTE: SBOM checks are disabled as SBOM tools are disabled (too large for LLM context) + // Check 10: SBOMSyfts CRD exists - DISABLED + + // Set summary + if result.Healthy { + result.Summary = "Kubescape is fully operational" + } else { + result.Summary = "Kubescape has issues that need attention" + result.Recommendations = recommendations + } + + content, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(content)), nil +} + +// handleListVulnerabilityManifests lists vulnerability manifests at image and workload levels +func (k *KubescapeTool) handleListVulnerabilityManifests(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if k.initError != nil { + toolErr := errors.NewKubescapeError("list_vulnerability_manifests", k.initError) + return toolErr.ToMCPResult(), nil + } + + namespace := mcp.ParseString(request, "namespace", "") + level := mcp.ParseString(request, "level", "both") + + // Build label selector based on level + labelSelector := "" + switch level { + case "workload": + labelSelector = "kubescape.io/context=filtered" + case "image": + labelSelector = "kubescape.io/context=non-filtered" + } + + // Determine namespace to query + queryNamespace := metav1.NamespaceAll + if namespace != "" { + queryNamespace = namespace + } + + // List manifests + listOpts := metav1.ListOptions{} + if labelSelector != "" { + listOpts.LabelSelector = labelSelector + } + + manifests, err := k.spdxClient.VulnerabilityManifests(queryNamespace).List(ctx, listOpts) + if err != nil { + toolErr := errors.NewKubescapeError("list_vulnerability_manifests", err). + WithContext("namespace", namespace). + WithContext("level", level) + return toolErr.ToMCPResult(), nil + } + + // Build response + vulnerabilityManifests := []map[string]interface{}{} + for _, manifest := range manifests.Items { + isImageLevel := manifest.Annotations[helpersv1.WlidMetadataKey] == "" + manifestMap := map[string]interface{}{ + "namespace": manifest.Namespace, + "manifest_name": manifest.Name, + "image_level": isImageLevel, + "workload_level": !isImageLevel, + "image_id": manifest.Annotations[helpersv1.ImageIDMetadataKey], + "image_tag": manifest.Annotations[helpersv1.ImageTagMetadataKey], + "workload_id": manifest.Annotations[helpersv1.WlidMetadataKey], + "workload_container_name": manifest.Annotations[helpersv1.ContainerNameMetadataKey], + "vulnerability_count": len(manifest.Spec.Payload.Matches), + } + vulnerabilityManifests = append(vulnerabilityManifests, manifestMap) + } + + result := map[string]interface{}{ + "vulnerability_manifests": vulnerabilityManifests, + "total_count": len(vulnerabilityManifests), + } + + content, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(content)), nil +} + +// handleListVulnerabilitiesInManifest lists all CVEs in a specific manifest +func (k *KubescapeTool) handleListVulnerabilitiesInManifest(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if k.initError != nil { + toolErr := errors.NewKubescapeError("list_vulnerabilities", k.initError) + return toolErr.ToMCPResult(), nil + } + + namespace := mcp.ParseString(request, "namespace", defaultKubescapeNamespace) + manifestName := mcp.ParseString(request, "manifest_name", "") + + if manifestName == "" { + return mcp.NewToolResultError("manifest_name parameter is required"), nil + } + + manifest, err := k.spdxClient.VulnerabilityManifests(namespace).Get(ctx, manifestName, metav1.GetOptions{}) + if err != nil { + toolErr := errors.NewKubescapeError("get_vulnerability_manifest", err). + WithContext("namespace", namespace). + WithContext("manifest_name", manifestName) + return toolErr.ToMCPResult(), nil + } + + // Extract vulnerabilities with summary info + vulnerabilities := []map[string]interface{}{} + severityCounts := map[string]int{ + "Critical": 0, + "High": 0, + "Medium": 0, + "Low": 0, + "Unknown": 0, + } + + for _, match := range manifest.Spec.Payload.Matches { + vuln := match.Vulnerability + severity := string(vuln.Severity) + if _, exists := severityCounts[severity]; exists { + severityCounts[severity]++ + } else { + severityCounts["Unknown"]++ + } + + vulnInfo := map[string]interface{}{ + "id": vuln.ID, + "severity": severity, + "description": truncateString(vuln.Description, 200), + "data_source": vuln.DataSource, + } + + if vuln.Fix.State != "" { + vulnInfo["fix_state"] = vuln.Fix.State + vulnInfo["fix_versions"] = vuln.Fix.Versions + } + + vulnerabilities = append(vulnerabilities, vulnInfo) + } + + result := map[string]interface{}{ + "manifest_name": manifestName, + "namespace": namespace, + "total_count": len(vulnerabilities), + "severity_summary": severityCounts, + "vulnerabilities": vulnerabilities, + } + + content, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(content)), nil +} + +// handleGetVulnerabilityDetails gets detailed info about a specific CVE in a manifest +func (k *KubescapeTool) handleGetVulnerabilityDetails(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if k.initError != nil { + toolErr := errors.NewKubescapeError("get_vulnerability_details", k.initError) + return toolErr.ToMCPResult(), nil + } + + namespace := mcp.ParseString(request, "namespace", defaultKubescapeNamespace) + manifestName := mcp.ParseString(request, "manifest_name", "") + cveID := mcp.ParseString(request, "cve_id", "") + + if manifestName == "" { + return mcp.NewToolResultError("manifest_name parameter is required"), nil + } + if cveID == "" { + return mcp.NewToolResultError("cve_id parameter is required"), nil + } + + manifest, err := k.spdxClient.VulnerabilityManifests(namespace).Get(ctx, manifestName, metav1.GetOptions{}) + if err != nil { + toolErr := errors.NewKubescapeError("get_vulnerability_manifest", err). + WithContext("namespace", namespace). + WithContext("manifest_name", manifestName) + return toolErr.ToMCPResult(), nil + } + + // Find matching CVE entries + var matches []v1beta1.Match + for _, m := range manifest.Spec.Payload.Matches { + if m.Vulnerability.ID == cveID { + matches = append(matches, m) + } + } + + if len(matches) == 0 { + return mcp.NewToolResultError(fmt.Sprintf("CVE %s not found in manifest %s", cveID, manifestName)), nil + } + + content, err := json.MarshalIndent(matches, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(content)), nil +} + +// handleListConfigurationScans lists configuration security scan results +func (k *KubescapeTool) handleListConfigurationScans(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if k.initError != nil { + toolErr := errors.NewKubescapeError("list_configuration_scans", k.initError) + return toolErr.ToMCPResult(), nil + } + + namespace := mcp.ParseString(request, "namespace", "") + + queryNamespace := metav1.NamespaceAll + if namespace != "" { + queryNamespace = namespace + } + + manifests, err := k.spdxClient.WorkloadConfigurationScans(queryNamespace).List(ctx, metav1.ListOptions{}) + if err != nil { + toolErr := errors.NewKubescapeError("list_configuration_scans", err). + WithContext("namespace", namespace) + return toolErr.ToMCPResult(), nil + } + + configManifests := []map[string]interface{}{} + for _, manifest := range manifests.Items { + item := map[string]interface{}{ + "namespace": manifest.Namespace, + "manifest_name": manifest.Name, + "created_at": manifest.CreationTimestamp.Format(time.RFC3339), + } + configManifests = append(configManifests, item) + } + + result := map[string]interface{}{ + "configuration_scans": configManifests, + "total_count": len(configManifests), + } + + content, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(content)), nil +} + +// handleGetConfigurationScan gets details of a specific configuration scan +func (k *KubescapeTool) handleGetConfigurationScan(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if k.initError != nil { + toolErr := errors.NewKubescapeError("get_configuration_scan", k.initError) + return toolErr.ToMCPResult(), nil + } + + namespace := mcp.ParseString(request, "namespace", defaultKubescapeNamespace) + manifestName := mcp.ParseString(request, "manifest_name", "") + + if manifestName == "" { + return mcp.NewToolResultError("manifest_name parameter is required"), nil + } + + manifest, err := k.spdxClient.WorkloadConfigurationScans(namespace).Get(ctx, manifestName, metav1.GetOptions{}) + if err != nil { + toolErr := errors.NewKubescapeError("get_configuration_scan", err). + WithContext("namespace", namespace). + WithContext("manifest_name", manifestName) + return toolErr.ToMCPResult(), nil + } + + content, err := json.MarshalIndent(manifest, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(content)), nil +} + +// handleListApplicationProfiles lists application profiles showing runtime behavior data +func (k *KubescapeTool) handleListApplicationProfiles(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if k.initError != nil { + toolErr := errors.NewKubescapeError("list_application_profiles", k.initError) + return toolErr.ToMCPResult(), nil + } + + namespace := mcp.ParseString(request, "namespace", "") + + queryNamespace := metav1.NamespaceAll + if namespace != "" { + queryNamespace = namespace + } + + profiles, err := k.spdxClient.ApplicationProfiles(queryNamespace).List(ctx, metav1.ListOptions{}) + if err != nil { + toolErr := errors.NewKubescapeError("list_application_profiles", err). + WithContext("namespace", namespace) + return toolErr.ToMCPResult(), nil + } + + profileList := []map[string]interface{}{} + for _, profile := range profiles.Items { + // Summarize what data is captured per container + containersCount := len(profile.Spec.Containers) + initContainersCount := len(profile.Spec.InitContainers) + ephemeralContainersCount := len(profile.Spec.EphemeralContainers) + + totalExecs := 0 + totalOpens := 0 + totalSyscalls := 0 + totalCapabilities := 0 + totalEndpoints := 0 + + for _, c := range profile.Spec.Containers { + totalExecs += len(c.Execs) + totalOpens += len(c.Opens) + totalSyscalls += len(c.Syscalls) + totalCapabilities += len(c.Capabilities) + totalEndpoints += len(c.Endpoints) + } + + profileMap := map[string]interface{}{ + "namespace": profile.Namespace, + "name": profile.Name, + "containers_count": containersCount, + "init_containers_count": initContainersCount, + "ephemeral_containers_count": ephemeralContainersCount, + "total_execs": totalExecs, + "total_opens": totalOpens, + "total_syscalls": totalSyscalls, + "total_capabilities": totalCapabilities, + "total_endpoints": totalEndpoints, + "created_at": profile.CreationTimestamp.Format(time.RFC3339), + } + profileList = append(profileList, profileMap) + } + + result := map[string]interface{}{ + "application_profiles": profileList, + "total_count": len(profileList), + "description": "ApplicationProfiles capture runtime behavior of workloads including: " + + "executed processes (Execs), file access patterns (Opens), system calls (Syscalls), " + + "Linux capabilities used, and HTTP endpoints accessed. " + + "Use this data to prioritize vulnerabilities - a CVE in an unused package is lower priority than one in an actively running process.", + } + + content, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(content)), nil +} + +// handleGetApplicationProfile gets detailed runtime behavior for a specific workload +func (k *KubescapeTool) handleGetApplicationProfile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if k.initError != nil { + toolErr := errors.NewKubescapeError("get_application_profile", k.initError) + return toolErr.ToMCPResult(), nil + } + + namespace := mcp.ParseString(request, "namespace", "") + name := mcp.ParseString(request, "name", "") + + if name == "" { + return mcp.NewToolResultError("name parameter is required"), nil + } + if namespace == "" { + return mcp.NewToolResultError("namespace parameter is required"), nil + } + + profile, err := k.spdxClient.ApplicationProfiles(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + toolErr := errors.NewKubescapeError("get_application_profile", err). + WithContext("namespace", namespace). + WithContext("name", name) + return toolErr.ToMCPResult(), nil + } + + // Build detailed response with container behaviors + containers := []map[string]interface{}{} + for _, c := range profile.Spec.Containers { + containerInfo := map[string]interface{}{ + "name": c.Name, + "execs": c.Execs, + "opens": c.Opens, + "syscalls": c.Syscalls, + "capabilities": c.Capabilities, + "endpoints": c.Endpoints, + } + if c.SeccompProfile.Name != "" || c.SeccompProfile.Path != "" { + containerInfo["seccomp_profile"] = c.SeccompProfile + } + containers = append(containers, containerInfo) + } + + initContainers := []map[string]interface{}{} + for _, c := range profile.Spec.InitContainers { + containerInfo := map[string]interface{}{ + "name": c.Name, + "execs": c.Execs, + "opens": c.Opens, + "syscalls": c.Syscalls, + "capabilities": c.Capabilities, + "endpoints": c.Endpoints, + } + initContainers = append(initContainers, containerInfo) + } + + result := map[string]interface{}{ + "namespace": namespace, + "name": name, + "containers": containers, + "init_containers": initContainers, + "annotations": profile.Annotations, + "labels": profile.Labels, + "description": "This ApplicationProfile shows what the workload containers actually execute at runtime. " + + "Execs: processes that run; Opens: files read/written; Syscalls: kernel-level operations; " + + "Capabilities: special Linux privileges; Endpoints: HTTP APIs called. " + + "Compare this with vulnerability findings to prioritize remediation - focus on CVEs affecting actively used components.", + } + + content, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(content)), nil +} + +// handleListNetworkNeighborhoods lists network communication patterns for workloads +func (k *KubescapeTool) handleListNetworkNeighborhoods(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if k.initError != nil { + toolErr := errors.NewKubescapeError("list_network_neighborhoods", k.initError) + return toolErr.ToMCPResult(), nil + } + + namespace := mcp.ParseString(request, "namespace", "") + + queryNamespace := metav1.NamespaceAll + if namespace != "" { + queryNamespace = namespace + } + + neighborhoods, err := k.spdxClient.NetworkNeighborhoods(queryNamespace).List(ctx, metav1.ListOptions{}) + if err != nil { + toolErr := errors.NewKubescapeError("list_network_neighborhoods", err). + WithContext("namespace", namespace) + return toolErr.ToMCPResult(), nil + } + + neighborhoodList := []map[string]interface{}{} + for _, nn := range neighborhoods.Items { + totalIngress := 0 + totalEgress := 0 + for _, c := range nn.Spec.Containers { + totalIngress += len(c.Ingress) + totalEgress += len(c.Egress) + } + + nnMap := map[string]interface{}{ + "namespace": nn.Namespace, + "name": nn.Name, + "containers_count": len(nn.Spec.Containers), + "total_ingress": totalIngress, + "total_egress": totalEgress, + "created_at": nn.CreationTimestamp.Format(time.RFC3339), + } + neighborhoodList = append(neighborhoodList, nnMap) + } + + result := map[string]interface{}{ + "network_neighborhoods": neighborhoodList, + "total_count": len(neighborhoodList), + "description": "NetworkNeighborhoods capture actual network communication patterns of workloads. " + + "Ingress: connections coming INTO the workload; Egress: connections going OUT from the workload. " + + "Includes DNS names, IP addresses, ports, and protocols. " + + "Use this data to understand attack surface and prioritize network-related security findings.", + } + + content, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(content)), nil +} + +// handleGetNetworkNeighborhood gets detailed network connections for a specific workload +func (k *KubescapeTool) handleGetNetworkNeighborhood(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if k.initError != nil { + toolErr := errors.NewKubescapeError("get_network_neighborhood", k.initError) + return toolErr.ToMCPResult(), nil + } + + namespace := mcp.ParseString(request, "namespace", "") + name := mcp.ParseString(request, "name", "") + + if name == "" { + return mcp.NewToolResultError("name parameter is required"), nil + } + if namespace == "" { + return mcp.NewToolResultError("namespace parameter is required"), nil + } + + nn, err := k.spdxClient.NetworkNeighborhoods(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + toolErr := errors.NewKubescapeError("get_network_neighborhood", err). + WithContext("namespace", namespace). + WithContext("name", name) + return toolErr.ToMCPResult(), nil + } + + // Build detailed response with container network data + containers := []map[string]interface{}{} + for _, c := range nn.Spec.Containers { + // Format ingress connections + ingressList := []map[string]interface{}{} + for _, ing := range c.Ingress { + ingressInfo := map[string]interface{}{ + "identifier": ing.Identifier, + "type": ing.Type, + } + if ing.DNS != "" { + ingressInfo["dns"] = ing.DNS + } + if len(ing.Ports) > 0 { + ingressInfo["ports"] = ing.Ports + } + if len(ing.IPAddress) > 0 { + ingressInfo["ip_address"] = ing.IPAddress + } + if ing.PodSelector != nil { + ingressInfo["pod_selector"] = ing.PodSelector + } + if ing.NamespaceSelector != nil { + ingressInfo["namespace_selector"] = ing.NamespaceSelector + } + ingressList = append(ingressList, ingressInfo) + } + + // Format egress connections + egressList := []map[string]interface{}{} + for _, egr := range c.Egress { + egressInfo := map[string]interface{}{ + "identifier": egr.Identifier, + "type": egr.Type, + } + if egr.DNS != "" { + egressInfo["dns"] = egr.DNS + } + if len(egr.Ports) > 0 { + egressInfo["ports"] = egr.Ports + } + if len(egr.IPAddress) > 0 { + egressInfo["ip_address"] = egr.IPAddress + } + if egr.PodSelector != nil { + egressInfo["pod_selector"] = egr.PodSelector + } + if egr.NamespaceSelector != nil { + egressInfo["namespace_selector"] = egr.NamespaceSelector + } + egressList = append(egressList, egressInfo) + } + + containerInfo := map[string]interface{}{ + "name": c.Name, + "ingress": ingressList, + "egress": egressList, + } + containers = append(containers, containerInfo) + } + + result := map[string]interface{}{ + "namespace": namespace, + "name": name, + "containers": containers, + "annotations": nn.Annotations, + "labels": nn.Labels, + "description": "This NetworkNeighborhood shows actual network connections observed for this workload. " + + "Ingress connections show what talks TO this workload. Egress connections show what this workload talks TO. " + + "Use this to verify if a workload with a vulnerability is actually exposed to the network.", + } + + content, err := json.MarshalIndent(result, "", " ") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(content)), nil +} + +// Helper function to truncate strings +func truncateString(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} + +// RegisterTools registers all Kubescape tools with the MCP server +func RegisterTools(s *server.MCPServer, kubeconfig string) { + tool := NewKubescapeTool(kubeconfig) + + // Health check tool + s.AddTool(mcp.NewTool("kubescape_check_health", + mcp.WithDescription("Check if Kubescape operator is installed and operational. Verifies namespace, operator pods, storage pods, CRDs, and scan data availability."), + mcp.WithString("namespace", mcp.Description("Namespace to check (default: kubescape)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("kubescape_check_health", tool.handleCheckHealth))) + + // List vulnerability manifests + s.AddTool(mcp.NewTool("kubescape_list_vulnerability_manifests", + mcp.WithDescription("List vulnerability manifests from Kubescape operator. Returns vulnerability scan results at image or workload level."), + mcp.WithString("namespace", mcp.Description("Filter by namespace (optional, defaults to all namespaces)")), + mcp.WithString("level", mcp.Description("Type of manifests to list: 'image', 'workload', or 'both' (default: both)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("kubescape_list_vulnerability_manifests", tool.handleListVulnerabilityManifests))) + + // List vulnerabilities in a manifest + s.AddTool(mcp.NewTool("kubescape_list_vulnerabilities", + mcp.WithDescription("List all CVEs/vulnerabilities found in a specific vulnerability manifest. Returns severity summary and vulnerability details."), + mcp.WithString("namespace", mcp.Description("Namespace of the manifest (default: kubescape)")), + mcp.WithString("manifest_name", mcp.Description("Name of the vulnerability manifest"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("kubescape_list_vulnerabilities", tool.handleListVulnerabilitiesInManifest))) + + // Get detailed vulnerability info + s.AddTool(mcp.NewTool("kubescape_get_vulnerability_details", + mcp.WithDescription("Get detailed information about a specific CVE in a vulnerability manifest, including affected packages and fix information."), + mcp.WithString("namespace", mcp.Description("Namespace of the manifest (default: kubescape)")), + mcp.WithString("manifest_name", mcp.Description("Name of the vulnerability manifest"), mcp.Required()), + mcp.WithString("cve_id", mcp.Description("CVE identifier (e.g., CVE-2023-12345)"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("kubescape_get_vulnerability_details", tool.handleGetVulnerabilityDetails))) + + // List configuration scans + s.AddTool(mcp.NewTool("kubescape_list_configuration_scans", + mcp.WithDescription("List configuration security scan results from Kubescape operator. Shows workloads that have been scanned for security misconfigurations."), + mcp.WithString("namespace", mcp.Description("Filter by namespace (optional, defaults to all namespaces)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("kubescape_list_configuration_scans", tool.handleListConfigurationScans))) + + // Get configuration scan details + s.AddTool(mcp.NewTool("kubescape_get_configuration_scan", + mcp.WithDescription("Get detailed configuration security scan results for a specific workload, including failed controls and remediation guidance."), + mcp.WithString("namespace", mcp.Description("Namespace of the scan (default: kubescape)")), + mcp.WithString("manifest_name", mcp.Description("Name of the configuration scan manifest"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("kubescape_get_configuration_scan", tool.handleGetConfigurationScan))) + + // List application profiles (runtime observability) + s.AddTool(mcp.NewTool("kubescape_list_application_profiles", + mcp.WithDescription("List ApplicationProfiles showing runtime behavior of workloads. These profiles capture: "+ + "executed processes (Execs), file access patterns (Opens), system calls (Syscalls), Linux capabilities used, and HTTP endpoints. "+ + "Use this data to prioritize vulnerability findings - a CVE in an unused package is lower priority than one in an actively running process. "+ + "Requires 'capabilities.runtimeObservability=enable' in Kubescape Helm chart."), + mcp.WithString("namespace", mcp.Description("Filter by namespace (optional, defaults to all namespaces)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("kubescape_list_application_profiles", tool.handleListApplicationProfiles))) + + // Get application profile details + s.AddTool(mcp.NewTool("kubescape_get_application_profile", + mcp.WithDescription("Get detailed runtime behavior profile for a specific workload. Shows what processes run, what files are accessed, "+ + "what system calls are made, and what capabilities are used per container. "+ + "Compare with CVE findings to prioritize remediation - focus on vulnerabilities affecting actively used components."), + mcp.WithString("namespace", mcp.Description("Namespace of the profile"), mcp.Required()), + mcp.WithString("name", mcp.Description("Name of the application profile"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("kubescape_get_application_profile", tool.handleGetApplicationProfile))) + + // List network neighborhoods (runtime observability) + s.AddTool(mcp.NewTool("kubescape_list_network_neighborhoods", + mcp.WithDescription("List NetworkNeighborhoods showing actual network communication patterns of workloads. "+ + "These capture: ingress connections (who talks TO the workload), egress connections (who the workload talks TO), "+ + "including DNS names, IP addresses, ports, and protocols. "+ + "Use this to understand attack surface and prioritize network-related security findings. "+ + "Requires 'capabilities.runtimeObservability=enable' in Kubescape Helm chart."), + mcp.WithString("namespace", mcp.Description("Filter by namespace (optional, defaults to all namespaces)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("kubescape_list_network_neighborhoods", tool.handleListNetworkNeighborhoods))) + + // Get network neighborhood details + s.AddTool(mcp.NewTool("kubescape_get_network_neighborhood", + mcp.WithDescription("Get detailed network connections for a specific workload. Shows all observed ingress and egress traffic "+ + "with DNS names, IPs, ports, and protocols. Use this to verify if a workload with a vulnerability is actually exposed to the network."), + mcp.WithString("namespace", mcp.Description("Namespace of the network neighborhood"), mcp.Required()), + mcp.WithString("name", mcp.Description("Name of the network neighborhood"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("kubescape_get_network_neighborhood", tool.handleGetNetworkNeighborhood))) + + // NOTE: SBOM tools are disabled as they return too much data for LLM context windows. + // SBOMs contain detailed package information that can be very large. + // To enable in the future, uncomment the handlers and tool registrations below. + // + // s.AddTool(mcp.NewTool("kubescape_list_sboms", ...)) + // s.AddTool(mcp.NewTool("kubescape_get_sbom", ...)) +} + +// Interfaces for testing - allows mocking the Kubernetes clients +type KubescapeToolInterface interface { + HandleCheckHealth(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + HandleListVulnerabilityManifests(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + HandleListVulnerabilitiesInManifest(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + HandleGetVulnerabilityDetails(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + HandleListConfigurationScans(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + HandleGetConfigurationScan(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + HandleListApplicationProfiles(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + HandleGetApplicationProfile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + HandleListNetworkNeighborhoods(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + HandleGetNetworkNeighborhood(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + // NOTE: SBOM handlers are disabled as they return too much data for LLM context + // HandleListSBOMs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + // HandleGetSBOM(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) +} + +// Ensure KubescapeTool implements the interface +var _ KubescapeToolInterface = (*KubescapeTool)(nil) + +// Export handler methods for testing +func (k *KubescapeTool) HandleCheckHealth(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.handleCheckHealth(ctx, request) +} + +func (k *KubescapeTool) HandleListVulnerabilityManifests(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.handleListVulnerabilityManifests(ctx, request) +} + +func (k *KubescapeTool) HandleListVulnerabilitiesInManifest(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.handleListVulnerabilitiesInManifest(ctx, request) +} + +func (k *KubescapeTool) HandleGetVulnerabilityDetails(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.handleGetVulnerabilityDetails(ctx, request) +} + +func (k *KubescapeTool) HandleListConfigurationScans(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.handleListConfigurationScans(ctx, request) +} + +func (k *KubescapeTool) HandleGetConfigurationScan(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.handleGetConfigurationScan(ctx, request) +} + +func (k *KubescapeTool) HandleListApplicationProfiles(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.handleListApplicationProfiles(ctx, request) +} + +func (k *KubescapeTool) HandleGetApplicationProfile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.handleGetApplicationProfile(ctx, request) +} + +func (k *KubescapeTool) HandleListNetworkNeighborhoods(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.handleListNetworkNeighborhoods(ctx, request) +} + +func (k *KubescapeTool) HandleGetNetworkNeighborhood(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return k.handleGetNetworkNeighborhood(ctx, request) +} + +// NOTE: SBOM handlers are disabled as they return too much data for LLM context +// func (k *KubescapeTool) HandleListSBOMs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +// return k.handleListSBOMs(ctx, request) +// } +// +// func (k *KubescapeTool) HandleGetSBOM(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { +// return k.handleGetSBOM(ctx, request) +// } diff --git a/pkg/kubescape/kubescape_test.go b/pkg/kubescape/kubescape_test.go new file mode 100644 index 00000000..6340b8d5 --- /dev/null +++ b/pkg/kubescape/kubescape_test.go @@ -0,0 +1,1193 @@ +package kubescape + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + kubescapefake "github.com/kubescape/storage/pkg/generated/clientset/versioned/fake" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubefake "k8s.io/client-go/kubernetes/fake" +) + +// Helper function to create a CallToolRequest with arguments +func makeRequest(args map[string]interface{}) mcp.CallToolRequest { + request := mcp.CallToolRequest{} + request.Params.Arguments = args + return request +} + +// Helper function to extract text content from MCP result +func getResultText(result *mcp.CallToolResult) string { + if result == nil || len(result.Content) == 0 { + return "" + } + if textContent, ok := result.Content[0].(mcp.TextContent); ok { + return textContent.Text + } + return "" +} + +func TestRegisterTools(t *testing.T) { + s := server.NewMCPServer("test", "1.0.0") + + // Should not panic + assert.NotPanics(t, func() { + RegisterTools(s, "") + }) + + // Verify tools are registered by checking the server has tools + // NOTE: SBOM tools are disabled (too large for LLM context), so we expect 10 tools + tools := s.ListTools() + assert.Len(t, tools, 10) + + expectedTools := map[string]bool{ + "kubescape_check_health": false, + "kubescape_list_vulnerability_manifests": false, + "kubescape_list_vulnerabilities": false, + "kubescape_get_vulnerability_details": false, + "kubescape_list_configuration_scans": false, + "kubescape_get_configuration_scan": false, + "kubescape_list_application_profiles": false, + "kubescape_get_application_profile": false, + "kubescape_list_network_neighborhoods": false, + "kubescape_get_network_neighborhood": false, + // NOTE: SBOM tools disabled - too large for LLM context + // "kubescape_list_sboms": false, + // "kubescape_get_sbom": false, + } + + for name := range tools { + if _, exists := expectedTools[name]; exists { + expectedTools[name] = true + } + } + + for name, found := range expectedTools { + assert.True(t, found, "Tool %s not found", name) + } +} + +func TestHandleCheckHealth_AllComponentsHealthy(t *testing.T) { + // Setup fake clients with all components healthy + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + k8sClient := kubefake.NewSimpleClientset( + // Namespace + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "kubescape"}}, + // Operator pods + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubescape-operator-123", + Namespace: "kubescape", + Labels: map[string]string{"app.kubernetes.io/name": "kubescape-operator"}, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + // Storage pods + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "storage-123", + Namespace: "kubescape", + Labels: map[string]string{"app.kubernetes.io/name": "storage"}, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + ) + + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + apiExtClient := apiextensionsfake.NewSimpleClientset( + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: vulnerabilityManifestsCRD}, + }, + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: workloadConfigurationScansCRD}, + }, + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: applicationProfilesCRD}, + }, + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: networkNeighborhoodsCRD}, + }, + // NOTE: SBOM CRD check is disabled (SBOM tools are too large for LLM context) + ) + + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.VulnerabilityManifest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-manifest", + Namespace: "kubescape", + }, + }, + &v1beta1.WorkloadConfigurationScan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-config-scan", + Namespace: "kubescape", + }, + }, + &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-app-profile", + Namespace: "kubescape", + }, + }, + &v1beta1.NetworkNeighborhood{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-network-neighborhood", + Namespace: "kubescape", + }, + }, + // NOTE: SBOM data check is disabled (SBOM tools are too large for LLM context) + ) + + tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleCheckHealth(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + // Parse the response + var health HealthCheckResult + err = json.Unmarshal([]byte(getResultText(result)), &health) + require.NoError(t, err) + + assert.True(t, health.Healthy) + assert.Equal(t, "ok", health.Checks["namespace"].Status) + assert.Equal(t, "ok", health.Checks["operator_pods"].Status) + assert.Equal(t, "ok", health.Checks["storage_pods"].Status) + assert.Equal(t, "ok", health.Checks["vulnerability_crd"].Status) + assert.Equal(t, "ok", health.Checks["configuration_crd"].Status) + assert.Equal(t, "ok", health.Checks["vulnerability_scan_data"].Status) + assert.Equal(t, "ok", health.Checks["configuration_scan_data"].Status) + assert.Equal(t, "ok", health.Checks["application_profiles_crd"].Status) + assert.Equal(t, "ok", health.Checks["application_profiles_data"].Status) + assert.Equal(t, "ok", health.Checks["network_neighborhoods_crd"].Status) + assert.Equal(t, "ok", health.Checks["network_neighborhoods_data"].Status) + // NOTE: SBOM checks are disabled (SBOM tools are too large for LLM context) + // assert.Equal(t, "ok", health.Checks["sbom_crd"].Status) + // assert.Equal(t, "ok", health.Checks["sbom_data"].Status) + assert.Equal(t, "Kubescape is fully operational", health.Summary) +} + +func TestHandleCheckHealth_NamespaceNotFound(t *testing.T) { + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + k8sClient := kubefake.NewSimpleClientset() // No namespace + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + apiExtClient := apiextensionsfake.NewSimpleClientset() + spdxClient := kubescapefake.NewSimpleClientset() + + tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleCheckHealth(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + var health HealthCheckResult + err = json.Unmarshal([]byte(getResultText(result)), &health) + require.NoError(t, err) + + assert.False(t, health.Healthy) + assert.Equal(t, "error", health.Checks["namespace"].Status) + assert.Contains(t, health.Checks["namespace"].Message, "not found") +} + +func TestHandleCheckHealth_OperatorPodsNotRunning(t *testing.T) { + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + k8sClient := kubefake.NewSimpleClientset( + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "kubescape"}}, + // No operator pods + ) + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + apiExtClient := apiextensionsfake.NewSimpleClientset() + spdxClient := kubescapefake.NewSimpleClientset() + + tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleCheckHealth(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + var health HealthCheckResult + err = json.Unmarshal([]byte(getResultText(result)), &health) + require.NoError(t, err) + + assert.False(t, health.Healthy) + assert.Equal(t, "error", health.Checks["operator_pods"].Status) + assert.Contains(t, health.Checks["operator_pods"].Message, "No operator pods found") + assert.Contains(t, health.Recommendations, "Install Kubescape operator: helm upgrade --install kubescape kubescape/kubescape-operator -n kubescape --create-namespace") +} + +func TestHandleCheckHealth_OperatorPodsUnhealthy(t *testing.T) { + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + k8sClient := kubefake.NewSimpleClientset( + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "kubescape"}}, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubescape-operator-123", + Namespace: "kubescape", + Labels: map[string]string{"app.kubernetes.io/name": "kubescape-operator"}, + }, + Status: corev1.PodStatus{Phase: corev1.PodPending}, // Not running + }, + ) + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + apiExtClient := apiextensionsfake.NewSimpleClientset() + spdxClient := kubescapefake.NewSimpleClientset() + + tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleCheckHealth(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + var health HealthCheckResult + err = json.Unmarshal([]byte(getResultText(result)), &health) + require.NoError(t, err) + + assert.False(t, health.Healthy) + assert.Equal(t, "warning", health.Checks["operator_pods"].Status) + assert.Contains(t, health.Checks["operator_pods"].Message, "0/1 pods running") +} + +func TestHandleCheckHealth_VulnerabilityCRDMissing(t *testing.T) { + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + k8sClient := kubefake.NewSimpleClientset( + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "kubescape"}}, + ) + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + apiExtClient := apiextensionsfake.NewSimpleClientset() // No CRDs + spdxClient := kubescapefake.NewSimpleClientset() + + tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleCheckHealth(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + var health HealthCheckResult + err = json.Unmarshal([]byte(getResultText(result)), &health) + require.NoError(t, err) + + assert.False(t, health.Healthy) + assert.Equal(t, "error", health.Checks["vulnerability_crd"].Status) + assert.Contains(t, health.Checks["vulnerability_crd"].Message, "not installed") +} + +func TestHandleCheckHealth_NoScanData(t *testing.T) { + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + k8sClient := kubefake.NewSimpleClientset( + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "kubescape"}}, + ) + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + apiExtClient := apiextensionsfake.NewSimpleClientset( + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: vulnerabilityManifestsCRD}, + }, + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: workloadConfigurationScansCRD}, + }, + ) + spdxClient := kubescapefake.NewSimpleClientset() // No vulnerability manifests + + tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleCheckHealth(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + var health HealthCheckResult + err = json.Unmarshal([]byte(getResultText(result)), &health) + require.NoError(t, err) + + // Warning for no scan data + assert.Equal(t, "warning", health.Checks["vulnerability_scan_data"].Status) + assert.Contains(t, health.Checks["vulnerability_scan_data"].Message, "No vulnerability manifests found") +} + +func TestHandleCheckHealth_RuntimeObservabilityCRDsMissing(t *testing.T) { + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + k8sClient := kubefake.NewSimpleClientset( + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "kubescape"}}, + ) + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + apiExtClient := apiextensionsfake.NewSimpleClientset( + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: vulnerabilityManifestsCRD}, + }, + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: workloadConfigurationScansCRD}, + }, + // No runtime observability CRDs (applicationprofiles, networkneighborhoods) + ) + spdxClient := kubescapefake.NewSimpleClientset() + + tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleCheckHealth(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + var health HealthCheckResult + err = json.Unmarshal([]byte(getResultText(result)), &health) + require.NoError(t, err) + + // Warning for missing runtime observability CRDs + assert.Equal(t, "warning", health.Checks["application_profiles_crd"].Status) + assert.Contains(t, health.Checks["application_profiles_crd"].Message, "not installed") + assert.Equal(t, "warning", health.Checks["network_neighborhoods_crd"].Status) + assert.Contains(t, health.Checks["network_neighborhoods_crd"].Message, "not installed") + + // Should have recommendation to enable runtime observability + foundRuntimeRecommendation := false + for _, r := range health.Recommendations { + if contains(r, "runtimeObservability") { + foundRuntimeRecommendation = true + break + } + } + assert.True(t, foundRuntimeRecommendation, "Expected recommendation to enable runtimeObservability") +} + +// Helper function for test +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr)) +} + +func containsHelper(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +func TestHandleCheckHealth_CustomNamespace(t *testing.T) { + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + k8sClient := kubefake.NewSimpleClientset( + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "custom-ns"}}, + ) + //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs + apiExtClient := apiextensionsfake.NewSimpleClientset() + spdxClient := kubescapefake.NewSimpleClientset() + + tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleCheckHealth(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "custom-ns", + })) + require.NoError(t, err) + require.NotNil(t, result) + + var health HealthCheckResult + err = json.Unmarshal([]byte(getResultText(result)), &health) + require.NoError(t, err) + + assert.Equal(t, "ok", health.Checks["namespace"].Status) + assert.Contains(t, health.Checks["namespace"].Message, "custom-ns") +} + +func TestHandleCheckHealth_InitError(t *testing.T) { + tool := NewKubescapeToolWithError(errors.New("failed to connect")) + + result, err := tool.HandleCheckHealth(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestHandleListVulnerabilityManifests_Success(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.VulnerabilityManifest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "manifest-1", + Namespace: "default", + Annotations: map[string]string{ + "kubescape.io/image-id": "sha256:abc123", + "kubescape.io/image-tag": "nginx:1.19", + }, + }, + Spec: v1beta1.VulnerabilityManifestSpec{ + Payload: v1beta1.GrypeDocument{ + Matches: []v1beta1.Match{ + {Vulnerability: v1beta1.Vulnerability{VulnerabilityMetadata: v1beta1.VulnerabilityMetadata{ID: "CVE-2021-1234"}}}, + }, + }, + }, + }, + &v1beta1.VulnerabilityManifest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "manifest-2", + Namespace: "kubescape", + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListVulnerabilityManifests(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(2), response["total_count"]) + manifests := response["vulnerability_manifests"].([]interface{}) + assert.Len(t, manifests, 2) +} + +func TestHandleListVulnerabilityManifests_FilterByNamespace(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.VulnerabilityManifest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "manifest-1", + Namespace: "default", + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListVulnerabilityManifests(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "default", + })) + require.NoError(t, err) + require.NotNil(t, result) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(1), response["total_count"]) +} + +func TestHandleListVulnerabilityManifests_EmptyResults(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListVulnerabilityManifests(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(0), response["total_count"]) +} + +func TestHandleListVulnerabilityManifests_InitError(t *testing.T) { + tool := NewKubescapeToolWithError(errors.New("failed to connect")) + + result, err := tool.HandleListVulnerabilityManifests(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestHandleListVulnerabilitiesInManifest_Success(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.VulnerabilityManifest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-manifest", + Namespace: "kubescape", + }, + Spec: v1beta1.VulnerabilityManifestSpec{ + Payload: v1beta1.GrypeDocument{ + Matches: []v1beta1.Match{ + { + Vulnerability: v1beta1.Vulnerability{ + VulnerabilityMetadata: v1beta1.VulnerabilityMetadata{ + ID: "CVE-2021-1234", + Severity: "Critical", + Description: "Test vulnerability", + }, + }, + }, + { + Vulnerability: v1beta1.Vulnerability{ + VulnerabilityMetadata: v1beta1.VulnerabilityMetadata{ + ID: "CVE-2021-5678", + Severity: "High", + }, + }, + }, + }, + }, + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListVulnerabilitiesInManifest(context.Background(), makeRequest(map[string]interface{}{ + "manifest_name": "test-manifest", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(2), response["total_count"]) + severitySummary := response["severity_summary"].(map[string]interface{}) + assert.Equal(t, float64(1), severitySummary["Critical"]) + assert.Equal(t, float64(1), severitySummary["High"]) +} + +func TestHandleListVulnerabilitiesInManifest_MissingManifestName(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListVulnerabilitiesInManifest(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "manifest_name parameter is required") +} + +func TestHandleListVulnerabilitiesInManifest_ManifestNotFound(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListVulnerabilitiesInManifest(context.Background(), makeRequest(map[string]interface{}{ + "manifest_name": "nonexistent", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestHandleGetVulnerabilityDetails_Success(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.VulnerabilityManifest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-manifest", + Namespace: "kubescape", + }, + Spec: v1beta1.VulnerabilityManifestSpec{ + Payload: v1beta1.GrypeDocument{ + Matches: []v1beta1.Match{ + { + Vulnerability: v1beta1.Vulnerability{ + VulnerabilityMetadata: v1beta1.VulnerabilityMetadata{ + ID: "CVE-2021-1234", + Severity: "Critical", + Description: "Test vulnerability", + }, + Fix: v1beta1.Fix{ + State: "fixed", + Versions: []string{"1.2.3"}, + }, + }, + }, + }, + }, + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetVulnerabilityDetails(context.Background(), makeRequest(map[string]interface{}{ + "manifest_name": "test-manifest", + "cve_id": "CVE-2021-1234", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError) + + var matches []v1beta1.Match + err = json.Unmarshal([]byte(getResultText(result)), &matches) + require.NoError(t, err) + + assert.Len(t, matches, 1) + assert.Equal(t, "CVE-2021-1234", matches[0].Vulnerability.ID) +} + +func TestHandleGetVulnerabilityDetails_MissingManifestName(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetVulnerabilityDetails(context.Background(), makeRequest(map[string]interface{}{ + "cve_id": "CVE-2021-1234", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "manifest_name parameter is required") +} + +func TestHandleGetVulnerabilityDetails_MissingCveId(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetVulnerabilityDetails(context.Background(), makeRequest(map[string]interface{}{ + "manifest_name": "test-manifest", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "cve_id parameter is required") +} + +func TestHandleGetVulnerabilityDetails_CveNotFound(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.VulnerabilityManifest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-manifest", + Namespace: "kubescape", + }, + Spec: v1beta1.VulnerabilityManifestSpec{ + Payload: v1beta1.GrypeDocument{ + Matches: []v1beta1.Match{}, + }, + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetVulnerabilityDetails(context.Background(), makeRequest(map[string]interface{}{ + "manifest_name": "test-manifest", + "cve_id": "CVE-2021-1234", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "CVE CVE-2021-1234 not found") +} + +func TestHandleListConfigurationScans_Success(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.WorkloadConfigurationScan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scan-1", + Namespace: "default", + }, + }, + &v1beta1.WorkloadConfigurationScan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scan-2", + Namespace: "kubescape", + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListConfigurationScans(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(2), response["total_count"]) +} + +func TestHandleListConfigurationScans_FilterByNamespace(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.WorkloadConfigurationScan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scan-1", + Namespace: "default", + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListConfigurationScans(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "default", + })) + require.NoError(t, err) + require.NotNil(t, result) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(1), response["total_count"]) +} + +func TestHandleListConfigurationScans_EmptyResults(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListConfigurationScans(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(0), response["total_count"]) +} + +func TestHandleGetConfigurationScan_Success(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.WorkloadConfigurationScan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-scan", + Namespace: "kubescape", + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetConfigurationScan(context.Background(), makeRequest(map[string]interface{}{ + "manifest_name": "test-scan", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError) +} + +func TestHandleGetConfigurationScan_MissingManifestName(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetConfigurationScan(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "manifest_name parameter is required") +} + +func TestHandleGetConfigurationScan_NotFound(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetConfigurationScan(context.Background(), makeRequest(map[string]interface{}{ + "manifest_name": "nonexistent", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestTruncateString(t *testing.T) { + tests := []struct { + name string + input string + maxLen int + expected string + }{ + {"short string", "hello", 10, "hello"}, + {"exact length", "hello", 5, "hello"}, + {"truncated", "hello world", 5, "hello..."}, + {"empty string", "", 10, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := truncateString(tt.input, tt.maxLen) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestNilArgumentsHandling(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + // Test with nil arguments map - should use defaults + request := mcp.CallToolRequest{} + request.Params.Arguments = nil + + result, err := tool.HandleListVulnerabilityManifests(context.Background(), request) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError) +} + +// Tests for ApplicationProfile handlers + +func TestHandleListApplicationProfiles_Success(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "profile-1", + Namespace: "default", + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "container-1", + Execs: []v1beta1.ExecCalls{ + {Path: "/bin/bash"}, + }, + Opens: []v1beta1.OpenCalls{ + {Path: "/etc/passwd"}, + }, + Syscalls: []string{"read", "write"}, + }, + }, + }, + }, + &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "profile-2", + Namespace: "kubescape", + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListApplicationProfiles(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(2), response["total_count"]) + assert.Contains(t, response["description"], "ApplicationProfiles capture runtime behavior") + profiles := response["application_profiles"].([]interface{}) + assert.Len(t, profiles, 2) +} + +func TestHandleListApplicationProfiles_FilterByNamespace(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "profile-1", + Namespace: "default", + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListApplicationProfiles(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "default", + })) + require.NoError(t, err) + require.NotNil(t, result) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(1), response["total_count"]) +} + +func TestHandleListApplicationProfiles_EmptyResults(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListApplicationProfiles(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(0), response["total_count"]) +} + +func TestHandleListApplicationProfiles_InitError(t *testing.T) { + tool := NewKubescapeToolWithError(errors.New("failed to connect")) + + result, err := tool.HandleListApplicationProfiles(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestHandleGetApplicationProfile_Success(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.ApplicationProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-profile", + Namespace: "default", + }, + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "container-1", + Execs: []v1beta1.ExecCalls{ + {Path: "/bin/bash"}, + }, + Opens: []v1beta1.OpenCalls{ + {Path: "/etc/passwd"}, + }, + Syscalls: []string{"read", "write"}, + Capabilities: []string{"NET_ADMIN"}, + }, + }, + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetApplicationProfile(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "default", + "name": "test-profile", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, "default", response["namespace"]) + assert.Equal(t, "test-profile", response["name"]) + assert.Contains(t, response["description"], "ApplicationProfile shows what the workload containers actually execute") +} + +func TestHandleGetApplicationProfile_MissingName(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetApplicationProfile(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "default", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "name parameter is required") +} + +func TestHandleGetApplicationProfile_MissingNamespace(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetApplicationProfile(context.Background(), makeRequest(map[string]interface{}{ + "name": "test-profile", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "namespace parameter is required") +} + +func TestHandleGetApplicationProfile_NotFound(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetApplicationProfile(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "default", + "name": "nonexistent", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) +} + +// Tests for NetworkNeighborhood handlers + +func TestHandleListNetworkNeighborhoods_Success(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.NetworkNeighborhood{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nn-1", + Namespace: "default", + }, + Spec: v1beta1.NetworkNeighborhoodSpec{ + Containers: []v1beta1.NetworkNeighborhoodContainer{ + { + Name: "container-1", + Ingress: []v1beta1.NetworkNeighbor{ + {Identifier: "pod-1", Type: "internal"}, + }, + Egress: []v1beta1.NetworkNeighbor{ + {Identifier: "api.example.com", Type: "external", DNS: "api.example.com"}, + }, + }, + }, + }, + }, + &v1beta1.NetworkNeighborhood{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nn-2", + Namespace: "kubescape", + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListNetworkNeighborhoods(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(2), response["total_count"]) + assert.Contains(t, response["description"], "NetworkNeighborhoods capture actual network communication patterns") + neighborhoods := response["network_neighborhoods"].([]interface{}) + assert.Len(t, neighborhoods, 2) +} + +func TestHandleListNetworkNeighborhoods_FilterByNamespace(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.NetworkNeighborhood{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nn-1", + Namespace: "default", + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListNetworkNeighborhoods(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "default", + })) + require.NoError(t, err) + require.NotNil(t, result) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(1), response["total_count"]) +} + +func TestHandleListNetworkNeighborhoods_EmptyResults(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleListNetworkNeighborhoods(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, float64(0), response["total_count"]) +} + +func TestHandleListNetworkNeighborhoods_InitError(t *testing.T) { + tool := NewKubescapeToolWithError(errors.New("failed to connect")) + + result, err := tool.HandleListNetworkNeighborhoods(context.Background(), makeRequest(nil)) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestHandleGetNetworkNeighborhood_Success(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset( + &v1beta1.NetworkNeighborhood{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-nn", + Namespace: "default", + }, + Spec: v1beta1.NetworkNeighborhoodSpec{ + Containers: []v1beta1.NetworkNeighborhoodContainer{ + { + Name: "container-1", + Ingress: []v1beta1.NetworkNeighbor{ + {Identifier: "pod-1", Type: "internal"}, + }, + Egress: []v1beta1.NetworkNeighbor{ + {Identifier: "api.example.com", Type: "external", DNS: "api.example.com"}, + }, + }, + }, + }, + }, + ) + + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetNetworkNeighborhood(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "default", + "name": "test-nn", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.IsError) + + var response map[string]interface{} + err = json.Unmarshal([]byte(getResultText(result)), &response) + require.NoError(t, err) + + assert.Equal(t, "default", response["namespace"]) + assert.Equal(t, "test-nn", response["name"]) + assert.Contains(t, response["description"], "NetworkNeighborhood shows actual network connections") +} + +func TestHandleGetNetworkNeighborhood_MissingName(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetNetworkNeighborhood(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "default", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "name parameter is required") +} + +func TestHandleGetNetworkNeighborhood_MissingNamespace(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetNetworkNeighborhood(context.Background(), makeRequest(map[string]interface{}{ + "name": "test-nn", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "namespace parameter is required") +} + +func TestHandleGetNetworkNeighborhood_NotFound(t *testing.T) { + spdxClient := kubescapefake.NewSimpleClientset() + tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) + + result, err := tool.HandleGetNetworkNeighborhood(context.Background(), makeRequest(map[string]interface{}{ + "namespace": "default", + "name": "nonexistent", + })) + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) +} + +// NOTE: SBOM tests are disabled as SBOM tools are too large for LLM context windows. +// The handlers still exist in the code but are not registered or exported. +// +// Tests for SBOM handlers - DISABLED +// +// func TestHandleListSBOMs_Success(t *testing.T) { ... } +// func TestHandleListSBOMs_FilterByNamespace(t *testing.T) { ... } +// func TestHandleListSBOMs_EmptyResults(t *testing.T) { ... } +// func TestHandleListSBOMs_InitError(t *testing.T) { ... } +// func TestHandleGetSBOM_Success(t *testing.T) { ... } +// func TestHandleGetSBOM_MissingName(t *testing.T) { ... } +// func TestHandleGetSBOM_MissingNamespace(t *testing.T) { ... } +// func TestHandleGetSBOM_NotFound(t *testing.T) { ... } diff --git a/pkg/kubescape/testing.go b/pkg/kubescape/testing.go new file mode 100644 index 00000000..093e3711 --- /dev/null +++ b/pkg/kubescape/testing.go @@ -0,0 +1,28 @@ +package kubescape + +import ( + spdxv1beta1 "github.com/kubescape/storage/pkg/generated/clientset/versioned/typed/softwarecomposition/v1beta1" + apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/client-go/kubernetes" +) + +// NewKubescapeToolWithClients creates a KubescapeTool with pre-configured clients for testing +func NewKubescapeToolWithClients( + k8sClient kubernetes.Interface, + apiExtClient apiextensionsclientset.Interface, + spdxClient spdxv1beta1.SpdxV1beta1Interface, +) *KubescapeTool { + return &KubescapeTool{ + k8sClient: k8sClient, + apiExtClient: apiExtClient, + spdxClient: spdxClient, + initError: nil, + } +} + +// NewKubescapeToolWithError creates a KubescapeTool with an initialization error for testing error paths +func NewKubescapeToolWithError(err error) *KubescapeTool { + return &KubescapeTool{ + initError: err, + } +} From abdd0d53d4fb11d6fa594f2e25936422e3ae6789 Mon Sep 17 00:00:00 2001 From: Matteo Mori <169821341+MatteoMori8@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:35:45 +0000 Subject: [PATCH 28/41] feat(helm): add enabledTools and extraArgs configuration options (#43) * feat(helm): add enabledTools and extraArgs configuration options Add support for configuring tool-server CLI arguments via Helm values: - `tools.enabledTools`: List of tool providers to enable (maps to --tools flag) - `tools.extraArgs`: Additional command-line arguments for future flags Example usage: ```yaml tools: enabledTools: - k8s - helm - prometheus extraArgs: - "--some-future-flag" ``` This is a non-breaking change - empty lists (default) preserve current behavior. Signed-off-by: Matteo Mori * refactor(helm): rename tools.extraArgs to tools.args Simplify the Helm values key name for additional CLI arguments. Signed-off-by: Matteo Mori --------- Signed-off-by: Matteo Mori --- helm/kagent-tools/templates/deployment.yaml | 6 ++++++ helm/kagent-tools/values.yaml | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/helm/kagent-tools/templates/deployment.yaml b/helm/kagent-tools/templates/deployment.yaml index ec8b8a80..001caef2 100644 --- a/helm/kagent-tools/templates/deployment.yaml +++ b/helm/kagent-tools/templates/deployment.yaml @@ -59,6 +59,12 @@ spec: args: - "--port" - "{{ .Values.service.ports.tools.targetPort }}" + {{- if .Values.tools.enabledTools }} + - "--tools={{ join "," .Values.tools.enabledTools }}" + {{- end }} + {{- with .Values.tools.args }} + {{- toYaml . | nindent 10 }} + {{- end }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.tools.image.registry }}/{{ .Values.tools.image.repository }}:{{ coalesce .Values.global.tag .Values.tools.image.tag .Chart.Version }}" diff --git a/helm/kagent-tools/values.yaml b/helm/kagent-tools/values.yaml index 40262a7f..556f56ef 100644 --- a/helm/kagent-tools/values.yaml +++ b/helm/kagent-tools/values.yaml @@ -6,6 +6,15 @@ global: tools: loglevel: "debug" + # List of tool providers to enable. Empty list means all tools are enabled. + # Available: k8s, helm, istio, cilium, argo, prometheus, kubescape, utils + enabledTools: [] + # - k8s + # - helm + # - prometheus + # Additional command-line arguments + args: [] + # - "--some-future-flag" image: registry: ghcr.io repository: kagent-dev/kagent/tools From c315417608c0e6b239e30685e356945050db0f05 Mon Sep 17 00:00:00 2001 From: Matteo Mori <169821341+MatteoMori8@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:36:04 +0000 Subject: [PATCH 29/41] feat(cli): add --read-only flag to disable write operations (#41) * feat: add --read-only flag to disable write operations Add a new `--read-only` CLI flag that disables tools which perform write operations (delete, patch, scale, create, apply, etc.). This enables deploying the MCP server in read-only mode for: - Observability-only use cases (monitoring, troubleshooting) - Environments with read-only service accounts - Compliance requirements separating read/write capabilities Tools are categorized as read-only or write operations: - K8s: 8 read-only, 14 write tools - Helm: 3 read-only, 3 write tools - Istio: 9 read-only, 3 write tools - Cilium: ~25 read-only, ~15 write tools - Argo: 4 read-only, 4 write tools - Prometheus/Kubescape/Utils: all read-only (unchanged)Signed-off-by: Matteo Mori * fix: disable shell tool in read-only mode The utils provider exposes a `shell` tool that executes arbitrary commands, bypassing read-only restrictions. In read-only mode, this tool is now disabled. Also pass readOnly to all providers (kubescape, prometheus, utils) for consistency with the existing providers. Signed-off-by: Matteo Mori --------- Signed-off-by: Matteo Mori --- cmd/main.go | 26 +-- pkg/argo/argo.go | 60 ++++--- pkg/cilium/cilium.go | 305 +++++++++++++++++--------------- pkg/cilium/cilium_test.go | 2 +- pkg/helm/helm.go | 61 ++++--- pkg/helm/helm_test.go | 2 +- pkg/istio/istio.go | 42 +++-- pkg/istio/istio_test.go | 2 +- pkg/k8s/k8s.go | 240 +++++++++++++------------ pkg/kubescape/kubescape.go | 2 +- pkg/kubescape/kubescape_test.go | 2 +- pkg/prometheus/prometheus.go | 2 +- pkg/utils/common.go | 40 +++-- 13 files changed, 419 insertions(+), 367 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index c1189e1d..374d7eea 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -39,6 +39,7 @@ var ( tools []string kubeconfig *string showVersion bool + readOnly bool // These variables should be set during build time using -ldflags Name = "kagent-tools-server" @@ -58,6 +59,7 @@ func init() { rootCmd.Flags().BoolVar(&stdio, "stdio", false, "Use stdio for communication instead of HTTP") rootCmd.Flags().StringSliceVar(&tools, "tools", []string{}, "List of tools to register. If empty, all tools are registered.") rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information and exit") + rootCmd.Flags().BoolVar(&readOnly, "read-only", false, "Run in read-only mode (disable tools that perform write operations)") kubeconfig = rootCmd.Flags().String("kubeconfig", "", "kubeconfig file path (optional, defaults to in-cluster config)") // if found .env file, load it @@ -119,9 +121,13 @@ func run(cmd *cobra.Command, args []string) { attribute.Bool("server.stdio_mode", stdio), attribute.Int("server.port", port), attribute.StringSlice("server.tools", tools), + attribute.Bool("server.read_only", readOnly), ) logger.Get().Info("Starting "+Name, "version", Version, "git_commit", GitCommit, "build_date", BuildDate) + if readOnly { + logger.Get().Info("Running in read-only mode - write operations are disabled") + } mcp := server.NewMCPServer( Name, @@ -129,7 +135,7 @@ func run(cmd *cobra.Command, args []string) { ) // Register tools - registerMCP(mcp, tools, *kubeconfig) + registerMCP(mcp, tools, *kubeconfig, readOnly) // Create wait group for server goroutines var wg sync.WaitGroup @@ -285,17 +291,17 @@ func runStdioServer(ctx context.Context, mcp *server.MCPServer) { } } -func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string) { +func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string, readOnly bool) { // A map to hold tool providers and their registration functions toolProviderMap := map[string]func(*server.MCPServer){ - "argo": argo.RegisterTools, - "cilium": cilium.RegisterTools, - "helm": helm.RegisterTools, - "istio": istio.RegisterTools, - "k8s": func(s *server.MCPServer) { k8s.RegisterTools(s, nil, kubeconfig) }, - "kubescape": func(s *server.MCPServer) { kubescape.RegisterTools(s, kubeconfig) }, - "prometheus": prometheus.RegisterTools, - "utils": utils.RegisterTools, + "argo": func(s *server.MCPServer) { argo.RegisterTools(s, readOnly) }, + "cilium": func(s *server.MCPServer) { cilium.RegisterTools(s, readOnly) }, + "helm": func(s *server.MCPServer) { helm.RegisterTools(s, readOnly) }, + "istio": func(s *server.MCPServer) { istio.RegisterTools(s, readOnly) }, + "k8s": func(s *server.MCPServer) { k8s.RegisterTools(s, nil, kubeconfig, readOnly) }, + "kubescape": func(s *server.MCPServer) { kubescape.RegisterTools(s, kubeconfig, readOnly) }, + "prometheus": func(s *server.MCPServer) { prometheus.RegisterTools(s, readOnly) }, + "utils": func(s *server.MCPServer) { utils.RegisterTools(s, readOnly) }, } // If no specific tools are specified, register all available tools. diff --git a/pkg/argo/argo.go b/pkg/argo/argo.go index 7edea840..758a4fb2 100644 --- a/pkg/argo/argo.go +++ b/pkg/argo/argo.go @@ -375,7 +375,8 @@ func handleListRollouts(ctx context.Context, request mcp.CallToolRequest) (*mcp. return mcp.NewToolResultText(output), nil } -func RegisterTools(s *server.MCPServer) { +func RegisterTools(s *server.MCPServer, readOnly bool) { + // Read-only tools - always registered s.AddTool(mcp.NewTool("argo_verify_argo_rollouts_controller_install", mcp.WithDescription("Verify that the Argo Rollouts controller is installed and running"), mcp.WithString("namespace", mcp.Description("The namespace where Argo Rollouts is installed")), @@ -392,36 +393,39 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("type", mcp.Description("What to list: rollouts or experiments"), mcp.DefaultString("rollouts")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_rollouts_list", handleListRollouts))) - s.AddTool(mcp.NewTool("argo_promote_rollout", - mcp.WithDescription("Promote a paused rollout to the next step"), - mcp.WithString("rollout_name", mcp.Description("The name of the rollout to promote"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), - mcp.WithString("full", mcp.Description("Promote the rollout to the final step")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_promote_rollout", handlePromoteRollout))) - - s.AddTool(mcp.NewTool("argo_pause_rollout", - mcp.WithDescription("Pause a rollout"), - mcp.WithString("rollout_name", mcp.Description("The name of the rollout to pause"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_pause_rollout", handlePauseRollout))) - - s.AddTool(mcp.NewTool("argo_set_rollout_image", - mcp.WithDescription("Set the image of a rollout"), - mcp.WithString("rollout_name", mcp.Description("The name of the rollout to set the image for"), mcp.Required()), - mcp.WithString("container_image", mcp.Description("The container image to set for the rollout"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_set_rollout_image", handleSetRolloutImage))) - - s.AddTool(mcp.NewTool("argo_verify_gateway_plugin", - mcp.WithDescription("Verify the installation status of the Argo Rollouts Gateway API plugin"), - mcp.WithString("version", mcp.Description("The version of the plugin to check")), - mcp.WithString("namespace", mcp.Description("The namespace for the plugin resources")), - mcp.WithString("should_install", mcp.Description("Whether to install the plugin if not found")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_verify_gateway_plugin", handleVerifyGatewayPlugin))) - s.AddTool(mcp.NewTool("argo_check_plugin_logs", mcp.WithDescription("Check the logs of the Argo Rollouts Gateway API plugin"), mcp.WithString("namespace", mcp.Description("The namespace of the plugin resources")), mcp.WithString("timeout", mcp.Description("Timeout for log collection in seconds")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_check_plugin_logs", handleCheckPluginLogs))) + + // Write tools - only registered when not in read-only mode + if !readOnly { + s.AddTool(mcp.NewTool("argo_promote_rollout", + mcp.WithDescription("Promote a paused rollout to the next step"), + mcp.WithString("rollout_name", mcp.Description("The name of the rollout to promote"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), + mcp.WithString("full", mcp.Description("Promote the rollout to the final step")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_promote_rollout", handlePromoteRollout))) + + s.AddTool(mcp.NewTool("argo_pause_rollout", + mcp.WithDescription("Pause a rollout"), + mcp.WithString("rollout_name", mcp.Description("The name of the rollout to pause"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_pause_rollout", handlePauseRollout))) + + s.AddTool(mcp.NewTool("argo_set_rollout_image", + mcp.WithDescription("Set the image of a rollout"), + mcp.WithString("rollout_name", mcp.Description("The name of the rollout to set the image for"), mcp.Required()), + mcp.WithString("container_image", mcp.Description("The container image to set for the rollout"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the rollout")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_set_rollout_image", handleSetRolloutImage))) + + s.AddTool(mcp.NewTool("argo_verify_gateway_plugin", + mcp.WithDescription("Verify the installation status of the Argo Rollouts Gateway API plugin"), + mcp.WithString("version", mcp.Description("The version of the plugin to check")), + mcp.WithString("namespace", mcp.Description("The namespace for the plugin resources")), + mcp.WithString("should_install", mcp.Description("Whether to install the plugin if not found")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("argo_verify_gateway_plugin", handleVerifyGatewayPlugin))) + } } diff --git a/pkg/cilium/cilium.go b/pkg/cilium/cilium.go index 6ad576cd..9479378c 100644 --- a/pkg/cilium/cilium.go +++ b/pkg/cilium/cilium.go @@ -200,41 +200,12 @@ func handleToggleClusterMesh(ctx context.Context, request mcp.CallToolRequest) ( return mcp.NewToolResultText(output), nil } -func RegisterTools(s *server.MCPServer) { - - // Register all Cilium tools (main and debug) +func RegisterTools(s *server.MCPServer, readOnly bool) { + // Read-only tools - always registered s.AddTool(mcp.NewTool("cilium_status_and_version", mcp.WithDescription("Get the status and version of Cilium installation"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_status_and_version", handleCiliumStatusAndVersion))) - s.AddTool(mcp.NewTool("cilium_upgrade_cilium", - mcp.WithDescription("Upgrade Cilium on the cluster"), - mcp.WithString("cluster_name", mcp.Description("The name of the cluster to upgrade Cilium on")), - mcp.WithString("datapath_mode", mcp.Description("The datapath mode to use for Cilium (tunnel, native, aws-eni, gke, azure, aks-byocni)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_upgrade_cilium", handleUpgradeCilium))) - - s.AddTool(mcp.NewTool("cilium_install_cilium", - mcp.WithDescription("Install Cilium on the cluster"), - mcp.WithString("cluster_name", mcp.Description("The name of the cluster to install Cilium on")), - mcp.WithString("cluster_id", mcp.Description("The ID of the cluster to install Cilium on")), - mcp.WithString("datapath_mode", mcp.Description("The datapath mode to use for Cilium (tunnel, native, aws-eni, gke, azure, aks-byocni)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_install_cilium", handleInstallCilium))) - - s.AddTool(mcp.NewTool("cilium_uninstall_cilium", - mcp.WithDescription("Uninstall Cilium from the cluster"), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_uninstall_cilium", handleUninstallCilium))) - - s.AddTool(mcp.NewTool("cilium_connect_to_remote_cluster", - mcp.WithDescription("Connect to a remote cluster for cluster mesh"), - mcp.WithString("cluster_name", mcp.Description("The name of the destination cluster"), mcp.Required()), - mcp.WithString("context", mcp.Description("The kubectl context for the destination cluster")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_connect_to_remote_cluster", handleConnectToRemoteCluster))) - - s.AddTool(mcp.NewTool("cilium_disconnect_remote_cluster", - mcp.WithDescription("Disconnect from a remote cluster"), - mcp.WithString("cluster_name", mcp.Description("The name of the destination cluster"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_disconnect_remote_cluster", handleDisconnectRemoteCluster))) - s.AddTool(mcp.NewTool("cilium_list_bgp_peers", mcp.WithDescription("List BGP peers"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_bgp_peers", handleListBGPPeers))) @@ -251,15 +222,46 @@ func RegisterTools(s *server.MCPServer) { mcp.WithDescription("Show Cilium features status"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_features_status", handleShowFeaturesStatus))) - s.AddTool(mcp.NewTool("cilium_toggle_hubble", - mcp.WithDescription("Enable or disable Hubble"), - mcp.WithString("enable", mcp.Description("Set to 'true' to enable, 'false' to disable")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_hubble", handleToggleHubble))) - - s.AddTool(mcp.NewTool("cilium_toggle_cluster_mesh", - mcp.WithDescription("Enable or disable cluster mesh"), - mcp.WithString("enable", mcp.Description("Set to 'true' to enable, 'false' to disable")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_cluster_mesh", handleToggleClusterMesh))) + // Write tools - only registered when write operations are enabled + if !readOnly { + s.AddTool(mcp.NewTool("cilium_upgrade_cilium", + mcp.WithDescription("Upgrade Cilium on the cluster"), + mcp.WithString("cluster_name", mcp.Description("The name of the cluster to upgrade Cilium on")), + mcp.WithString("datapath_mode", mcp.Description("The datapath mode to use for Cilium (tunnel, native, aws-eni, gke, azure, aks-byocni)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_upgrade_cilium", handleUpgradeCilium))) + + s.AddTool(mcp.NewTool("cilium_install_cilium", + mcp.WithDescription("Install Cilium on the cluster"), + mcp.WithString("cluster_name", mcp.Description("The name of the cluster to install Cilium on")), + mcp.WithString("cluster_id", mcp.Description("The ID of the cluster to install Cilium on")), + mcp.WithString("datapath_mode", mcp.Description("The datapath mode to use for Cilium (tunnel, native, aws-eni, gke, azure, aks-byocni)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_install_cilium", handleInstallCilium))) + + s.AddTool(mcp.NewTool("cilium_uninstall_cilium", + mcp.WithDescription("Uninstall Cilium from the cluster"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_uninstall_cilium", handleUninstallCilium))) + + s.AddTool(mcp.NewTool("cilium_connect_to_remote_cluster", + mcp.WithDescription("Connect to a remote cluster for cluster mesh"), + mcp.WithString("cluster_name", mcp.Description("The name of the destination cluster"), mcp.Required()), + mcp.WithString("context", mcp.Description("The kubectl context for the destination cluster")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_connect_to_remote_cluster", handleConnectToRemoteCluster))) + + s.AddTool(mcp.NewTool("cilium_disconnect_remote_cluster", + mcp.WithDescription("Disconnect from a remote cluster"), + mcp.WithString("cluster_name", mcp.Description("The name of the destination cluster"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_disconnect_remote_cluster", handleDisconnectRemoteCluster))) + + s.AddTool(mcp.NewTool("cilium_toggle_hubble", + mcp.WithDescription("Enable or disable Hubble"), + mcp.WithString("enable", mcp.Description("Set to 'true' to enable, 'false' to disable")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_hubble", handleToggleHubble))) + + s.AddTool(mcp.NewTool("cilium_toggle_cluster_mesh", + mcp.WithDescription("Enable or disable cluster mesh"), + mcp.WithString("enable", mcp.Description("Set to 'true' to enable, 'false' to disable")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_cluster_mesh", handleToggleClusterMesh))) + } // Add tools that are also needed by cilium-manager agent s.AddTool(mcp.NewTool("cilium_get_daemon_status", @@ -295,12 +297,15 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to show the configuration options for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_configuration_options", handleShowConfigurationOptions))) - s.AddTool(mcp.NewTool("cilium_toggle_configuration_option", - mcp.WithDescription("Toggle a Cilium configuration option"), - mcp.WithString("option", mcp.Description("The option to toggle"), mcp.Required()), - mcp.WithString("value", mcp.Description("The value to set the option to (true/false)"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to toggle the configuration option for")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_configuration_option", handleToggleConfigurationOption))) + // Write tool - toggle_configuration_option + if !readOnly { + s.AddTool(mcp.NewTool("cilium_toggle_configuration_option", + mcp.WithDescription("Toggle a Cilium configuration option"), + mcp.WithString("option", mcp.Description("The option to toggle"), mcp.Required()), + mcp.WithString("value", mcp.Description("The value to set the option to (true/false)"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to toggle the configuration option for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_toggle_configuration_option", handleToggleConfigurationOption))) + } s.AddTool(mcp.NewTool("cilium_list_services", mcp.WithDescription("List services for the cluster"), @@ -314,31 +319,34 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the service information for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_service_information", handleGetServiceInformation))) - s.AddTool(mcp.NewTool("cilium_update_service", - mcp.WithDescription("Update a service in the cluster"), - mcp.WithString("backend_weights", mcp.Description("The backend weights to update the service with")), - mcp.WithString("backends", mcp.Description("The backends to update the service with"), mcp.Required()), - mcp.WithString("frontend", mcp.Description("The frontend to update the service with"), mcp.Required()), - mcp.WithString("id", mcp.Description("The ID of the service to update"), mcp.Required()), - mcp.WithString("k8s_cluster_internal", mcp.Description("Whether to update the k8s cluster internal flag")), - mcp.WithString("k8s_ext_traffic_policy", mcp.Description("The k8s ext traffic policy to update the service with")), - mcp.WithString("k8s_external", mcp.Description("Whether to update the k8s external flag")), - mcp.WithString("k8s_host_port", mcp.Description("Whether to update the k8s host port flag")), - mcp.WithString("k8s_int_traffic_policy", mcp.Description("The k8s int traffic policy to update the service with")), - mcp.WithString("k8s_load_balancer", mcp.Description("Whether to update the k8s load balancer flag")), - mcp.WithString("k8s_node_port", mcp.Description("Whether to update the k8s node port flag")), - mcp.WithString("local_redirect", mcp.Description("Whether to update the local redirect flag")), - mcp.WithString("protocol", mcp.Description("The protocol to update the service with")), - mcp.WithString("states", mcp.Description("The states to update the service with")), - mcp.WithString("node_name", mcp.Description("The name of the node to update the service on")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_service", handleUpdateService))) - - s.AddTool(mcp.NewTool("cilium_delete_service", - mcp.WithDescription("Delete a service from the cluster"), - mcp.WithString("service_id", mcp.Description("The ID of the service to delete")), - mcp.WithString("all", mcp.Description("Whether to delete all services (true/false)")), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the service from")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_service", handleDeleteService))) + // Write tools - service management + if !readOnly { + s.AddTool(mcp.NewTool("cilium_update_service", + mcp.WithDescription("Update a service in the cluster"), + mcp.WithString("backend_weights", mcp.Description("The backend weights to update the service with")), + mcp.WithString("backends", mcp.Description("The backends to update the service with"), mcp.Required()), + mcp.WithString("frontend", mcp.Description("The frontend to update the service with"), mcp.Required()), + mcp.WithString("id", mcp.Description("The ID of the service to update"), mcp.Required()), + mcp.WithString("k8s_cluster_internal", mcp.Description("Whether to update the k8s cluster internal flag")), + mcp.WithString("k8s_ext_traffic_policy", mcp.Description("The k8s ext traffic policy to update the service with")), + mcp.WithString("k8s_external", mcp.Description("Whether to update the k8s external flag")), + mcp.WithString("k8s_host_port", mcp.Description("Whether to update the k8s host port flag")), + mcp.WithString("k8s_int_traffic_policy", mcp.Description("The k8s int traffic policy to update the service with")), + mcp.WithString("k8s_load_balancer", mcp.Description("Whether to update the k8s load balancer flag")), + mcp.WithString("k8s_node_port", mcp.Description("Whether to update the k8s node port flag")), + mcp.WithString("local_redirect", mcp.Description("Whether to update the local redirect flag")), + mcp.WithString("protocol", mcp.Description("The protocol to update the service with")), + mcp.WithString("states", mcp.Description("The states to update the service with")), + mcp.WithString("node_name", mcp.Description("The name of the node to update the service on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_service", handleUpdateService))) + + s.AddTool(mcp.NewTool("cilium_delete_service", + mcp.WithDescription("Delete a service from the cluster"), + mcp.WithString("service_id", mcp.Description("The ID of the service to delete")), + mcp.WithString("all", mcp.Description("Whether to delete all services (true/false)")), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the service from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_service", handleDeleteService))) + } // Debug tools (previously in RegisterCiliumDbgTools) s.AddTool(mcp.NewTool("cilium_get_endpoint_details", @@ -361,26 +369,29 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the endpoint health for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_endpoint_health", handleGetEndpointHealth))) - s.AddTool(mcp.NewTool("cilium_manage_endpoint_labels", - mcp.WithDescription("Manage the labels (add or delete) of an endpoint in the cluster"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage labels for"), mcp.Required()), - mcp.WithString("labels", mcp.Description("Space-separated labels to manage (e.g., 'key1=value1 key2=value2')"), mcp.Required()), - mcp.WithString("action", mcp.Description("The action to perform on the labels (add or delete)"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint labels on")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_manage_endpoint_labels", handleManageEndpointLabels))) - - s.AddTool(mcp.NewTool("cilium_manage_endpoint_config", - mcp.WithDescription("Manage the configuration of an endpoint in the cluster"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage configuration for"), mcp.Required()), - mcp.WithString("config", mcp.Description("The configuration to manage for the endpoint provided as a space-separated list of key-value pairs (e.g. 'DropNotification=false TraceNotification=false')"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint configuration on")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_manage_endpoint_config", handleManageEndpointConfiguration))) - - s.AddTool(mcp.NewTool("cilium_disconnect_endpoint", - mcp.WithDescription("Disconnect an endpoint from the network"), - mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to disconnect"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to disconnect the endpoint from")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_disconnect_endpoint", handleDisconnectEndpoint))) + // Write tools - endpoint management + if !readOnly { + s.AddTool(mcp.NewTool("cilium_manage_endpoint_labels", + mcp.WithDescription("Manage the labels (add or delete) of an endpoint in the cluster"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage labels for"), mcp.Required()), + mcp.WithString("labels", mcp.Description("Space-separated labels to manage (e.g., 'key1=value1 key2=value2')"), mcp.Required()), + mcp.WithString("action", mcp.Description("The action to perform on the labels (add or delete)"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint labels on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_manage_endpoint_labels", handleManageEndpointLabels))) + + s.AddTool(mcp.NewTool("cilium_manage_endpoint_config", + mcp.WithDescription("Manage the configuration of an endpoint in the cluster"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to manage configuration for"), mcp.Required()), + mcp.WithString("config", mcp.Description("The configuration to manage for the endpoint provided as a space-separated list of key-value pairs (e.g. 'DropNotification=false TraceNotification=false')"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to manage the endpoint configuration on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_manage_endpoint_config", handleManageEndpointConfiguration))) + + s.AddTool(mcp.NewTool("cilium_disconnect_endpoint", + mcp.WithDescription("Disconnect an endpoint from the network"), + mcp.WithString("endpoint_id", mcp.Description("The ID of the endpoint to disconnect"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to disconnect the endpoint from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_disconnect_endpoint", handleDisconnectEndpoint))) + } s.AddTool(mcp.NewTool("cilium_list_identities", mcp.WithDescription("List all identities in the cluster"), @@ -403,10 +414,13 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the encryption state for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_display_encryption_state", handleDisplayEncryptionState))) - s.AddTool(mcp.NewTool("cilium_flush_ipsec_state", - mcp.WithDescription("Flush the IPsec state for the cluster"), - mcp.WithString("node_name", mcp.Description("The name of the node to flush the IPsec state for")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_flush_ipsec_state", handleFlushIPsecState))) + // Write tool - flush_ipsec_state + if !readOnly { + s.AddTool(mcp.NewTool("cilium_flush_ipsec_state", + mcp.WithDescription("Flush the IPsec state for the cluster"), + mcp.WithString("node_name", mcp.Description("The name of the node to flush the IPsec state for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_flush_ipsec_state", handleFlushIPsecState))) + } s.AddTool(mcp.NewTool("cilium_list_envoy_config", mcp.WithDescription("List the Envoy configuration for a resource in the cluster"), @@ -437,11 +451,14 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the IP cache information for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_show_ip_cache_information", handleShowIPCacheInformation))) - s.AddTool(mcp.NewTool("cilium_delete_key_from_kv_store", - mcp.WithDescription("Delete a key from the kvstore for the cluster"), - mcp.WithString("key", mcp.Description("The key to delete from the kvstore"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the key from")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_key_from_kv_store", handleDeleteKeyFromKVStore))) + // Write tool - delete_key_from_kv_store + if !readOnly { + s.AddTool(mcp.NewTool("cilium_delete_key_from_kv_store", + mcp.WithDescription("Delete a key from the kvstore for the cluster"), + mcp.WithString("key", mcp.Description("The key to delete from the kvstore"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the key from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_key_from_kv_store", handleDeleteKeyFromKVStore))) + } s.AddTool(mcp.NewTool("cilium_get_kv_store_key", mcp.WithDescription("Get a key from the kvstore for the cluster"), @@ -449,12 +466,15 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the key from")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_kv_store_key", handleGetKVStoreKey))) - s.AddTool(mcp.NewTool("cilium_set_kv_store_key", - mcp.WithDescription("Set a key in the kvstore for the cluster"), - mcp.WithString("key", mcp.Description("The key to set in the kvstore"), mcp.Required()), - mcp.WithString("value", mcp.Description("The value to set in the kvstore"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to set the key in")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_set_kv_store_key", handleSetKVStoreKey))) + // Write tool - set_kv_store_key + if !readOnly { + s.AddTool(mcp.NewTool("cilium_set_kv_store_key", + mcp.WithDescription("Set a key in the kvstore for the cluster"), + mcp.WithString("key", mcp.Description("The key to set in the kvstore"), mcp.Required()), + mcp.WithString("value", mcp.Description("The value to set in the kvstore"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to set the key in")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_set_kv_store_key", handleSetKVStoreKey))) + } s.AddTool(mcp.NewTool("cilium_show_load_information", mcp.WithDescription("Show load information for the cluster"), @@ -505,12 +525,15 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get policy node information for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_display_policy_node_information", handleDisplayPolicyNodeInformation))) - s.AddTool(mcp.NewTool("cilium_delete_policy_rules", - mcp.WithDescription("Delete policy rules for the cluster"), - mcp.WithString("labels", mcp.Description("The labels to delete policy rules for")), - mcp.WithString("all", mcp.Description("Whether to delete all policy rules")), - mcp.WithString("node_name", mcp.Description("The name of the node to delete policy rules for")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_policy_rules", handleDeletePolicyRules))) + // Write tool - delete_policy_rules + if !readOnly { + s.AddTool(mcp.NewTool("cilium_delete_policy_rules", + mcp.WithDescription("Delete policy rules for the cluster"), + mcp.WithString("labels", mcp.Description("The labels to delete policy rules for")), + mcp.WithString("all", mcp.Description("Whether to delete all policy rules")), + mcp.WithString("node_name", mcp.Description("The name of the node to delete policy rules for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_policy_rules", handleDeletePolicyRules))) + } s.AddTool(mcp.NewTool("cilium_display_selectors", mcp.WithDescription("Display selectors for the cluster"), @@ -522,19 +545,22 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the XDP CIDR filters for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_list_xdp_cidr_filters", handleListXDPCIDRFilters))) - s.AddTool(mcp.NewTool("cilium_update_xdp_cidr_filters", - mcp.WithDescription("Update XDP CIDR filters for the cluster"), - mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to update the XDP filters for"), mcp.Required()), - mcp.WithString("revision", mcp.Description("The revision of the XDP filters to update")), - mcp.WithString("node_name", mcp.Description("The name of the node to update the XDP filters for")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_xdp_cidr_filters", handleUpdateXDPCIDRFilters))) + // Write tools - XDP CIDR filters + if !readOnly { + s.AddTool(mcp.NewTool("cilium_update_xdp_cidr_filters", + mcp.WithDescription("Update XDP CIDR filters for the cluster"), + mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to update the XDP filters for"), mcp.Required()), + mcp.WithString("revision", mcp.Description("The revision of the XDP filters to update")), + mcp.WithString("node_name", mcp.Description("The name of the node to update the XDP filters for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_xdp_cidr_filters", handleUpdateXDPCIDRFilters))) - s.AddTool(mcp.NewTool("cilium_delete_xdp_cidr_filters", - mcp.WithDescription("Delete XDP CIDR filters for the cluster"), - mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to delete the XDP filters for"), mcp.Required()), - mcp.WithString("revision", mcp.Description("The revision of the XDP filters to delete")), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the XDP filters for")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_xdp_cidr_filters", handleDeleteXDPCIDRFilters))) + s.AddTool(mcp.NewTool("cilium_delete_xdp_cidr_filters", + mcp.WithDescription("Delete XDP CIDR filters for the cluster"), + mcp.WithString("cidr_prefixes", mcp.Description("The CIDR prefixes to delete the XDP filters for"), mcp.Required()), + mcp.WithString("revision", mcp.Description("The revision of the XDP filters to delete")), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the XDP filters for")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_xdp_cidr_filters", handleDeleteXDPCIDRFilters))) + } s.AddTool(mcp.NewTool("cilium_validate_cilium_network_policies", mcp.WithDescription("Validate Cilium network policies for the cluster"), @@ -554,20 +580,23 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("node_name", mcp.Description("The name of the node to get the PCAP recorder for")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_get_pcap_recorder", handleGetPCAPRecorder))) - s.AddTool(mcp.NewTool("cilium_delete_pcap_recorder", - mcp.WithDescription("Delete a PCAP recorder for the cluster"), - mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to delete"), mcp.Required()), - mcp.WithString("node_name", mcp.Description("The name of the node to delete the PCAP recorder from")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_pcap_recorder", handleDeletePCAPRecorder))) - - s.AddTool(mcp.NewTool("cilium_update_pcap_recorder", - mcp.WithDescription("Update a PCAP recorder for the cluster"), - mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to update"), mcp.Required()), - mcp.WithString("filters", mcp.Description("The filters to update the PCAP recorder with"), mcp.Required()), - mcp.WithString("caplen", mcp.Description("The caplen to update the PCAP recorder with")), - mcp.WithString("id", mcp.Description("The id to update the PCAP recorder with")), - mcp.WithString("node_name", mcp.Description("The name of the node to update the PCAP recorder on")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_pcap_recorder", handleUpdatePCAPRecorder))) + // Write tools - PCAP recorder management + if !readOnly { + s.AddTool(mcp.NewTool("cilium_delete_pcap_recorder", + mcp.WithDescription("Delete a PCAP recorder for the cluster"), + mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to delete"), mcp.Required()), + mcp.WithString("node_name", mcp.Description("The name of the node to delete the PCAP recorder from")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_delete_pcap_recorder", handleDeletePCAPRecorder))) + + s.AddTool(mcp.NewTool("cilium_update_pcap_recorder", + mcp.WithDescription("Update a PCAP recorder for the cluster"), + mcp.WithString("recorder_id", mcp.Description("The ID of the PCAP recorder to update"), mcp.Required()), + mcp.WithString("filters", mcp.Description("The filters to update the PCAP recorder with"), mcp.Required()), + mcp.WithString("caplen", mcp.Description("The caplen to update the PCAP recorder with")), + mcp.WithString("id", mcp.Description("The id to update the PCAP recorder with")), + mcp.WithString("node_name", mcp.Description("The name of the node to update the PCAP recorder on")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("cilium_update_pcap_recorder", handleUpdatePCAPRecorder))) + } } // -- Debug Tools -- diff --git a/pkg/cilium/cilium_test.go b/pkg/cilium/cilium_test.go index b7827de4..5e4ec243 100644 --- a/pkg/cilium/cilium_test.go +++ b/pkg/cilium/cilium_test.go @@ -16,7 +16,7 @@ import ( func TestRegisterCiliumTools(t *testing.T) { s := server.NewMCPServer("test-server", "v0.0.1") - RegisterTools(s) + RegisterTools(s, false) // false = enable all tools including write operations // We can't directly check the tools, but we can ensure the call doesn't panic } diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 0bf1a4c8..c8a6b917 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -288,8 +288,8 @@ func handleHelmRepoUpdate(ctx context.Context, request mcp.CallToolRequest) (*mc } // Register Helm tools -func RegisterTools(s *server.MCPServer) { - +func RegisterTools(s *server.MCPServer, readOnly bool) { + // Read-only tools - always registered s.AddTool(mcp.NewTool("helm_list_releases", mcp.WithDescription("List Helm releases in a namespace"), mcp.WithString("namespace", mcp.Description("The namespace to list releases from")), @@ -311,34 +311,37 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("resource", mcp.Description("The resource to get (all, hooks, manifest, notes, values)")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_get_release", handleHelmGetRelease))) - s.AddTool(mcp.NewTool("helm_upgrade", - mcp.WithDescription("Upgrade or install a Helm release"), - mcp.WithString("name", mcp.Description("The name of the release"), mcp.Required()), - mcp.WithString("chart", mcp.Description("The chart to install or upgrade to"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the release")), - mcp.WithString("version", mcp.Description("The version of the chart to upgrade to")), - mcp.WithString("values", mcp.Description("Path to a values file")), - mcp.WithString("set", mcp.Description("Set values on the command line (e.g., 'key1=val1,key2=val2')")), - mcp.WithString("install", mcp.Description("Run an install if the release is not present")), - mcp.WithString("dry_run", mcp.Description("Simulate an upgrade")), - mcp.WithString("wait", mcp.Description("Wait for the upgrade to complete")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_upgrade", handleHelmUpgradeRelease))) - - s.AddTool(mcp.NewTool("helm_uninstall", - mcp.WithDescription("Uninstall a Helm release"), - mcp.WithString("name", mcp.Description("The name of the release to uninstall"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the release"), mcp.Required()), - mcp.WithString("dry_run", mcp.Description("Simulate an uninstall")), - mcp.WithString("wait", mcp.Description("Wait for the uninstall to complete")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_uninstall", handleHelmUninstall))) - - s.AddTool(mcp.NewTool("helm_repo_add", - mcp.WithDescription("Add a Helm repository"), - mcp.WithString("name", mcp.Description("The name of the repository"), mcp.Required()), - mcp.WithString("url", mcp.Description("The URL of the repository"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_repo_add", handleHelmRepoAdd))) - s.AddTool(mcp.NewTool("helm_repo_update", mcp.WithDescription("Update information of available charts locally from chart repositories"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_repo_update", handleHelmRepoUpdate))) + + // Write tools - only registered when not in read-only mode + if !readOnly { + s.AddTool(mcp.NewTool("helm_upgrade", + mcp.WithDescription("Upgrade or install a Helm release"), + mcp.WithString("name", mcp.Description("The name of the release"), mcp.Required()), + mcp.WithString("chart", mcp.Description("The chart to install or upgrade to"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the release")), + mcp.WithString("version", mcp.Description("The version of the chart to upgrade to")), + mcp.WithString("values", mcp.Description("Path to a values file")), + mcp.WithString("set", mcp.Description("Set values on the command line (e.g., 'key1=val1,key2=val2')")), + mcp.WithString("install", mcp.Description("Run an install if the release is not present")), + mcp.WithString("dry_run", mcp.Description("Simulate an upgrade")), + mcp.WithString("wait", mcp.Description("Wait for the upgrade to complete")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_upgrade", handleHelmUpgradeRelease))) + + s.AddTool(mcp.NewTool("helm_uninstall", + mcp.WithDescription("Uninstall a Helm release"), + mcp.WithString("name", mcp.Description("The name of the release to uninstall"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the release"), mcp.Required()), + mcp.WithString("dry_run", mcp.Description("Simulate an uninstall")), + mcp.WithString("wait", mcp.Description("Wait for the uninstall to complete")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_uninstall", handleHelmUninstall))) + + s.AddTool(mcp.NewTool("helm_repo_add", + mcp.WithDescription("Add a Helm repository"), + mcp.WithString("name", mcp.Description("The name of the repository"), mcp.Required()), + mcp.WithString("url", mcp.Description("The URL of the repository"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("helm_repo_add", handleHelmRepoAdd))) + } } diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go index 28dca31a..9e5b26ca 100644 --- a/pkg/helm/helm_test.go +++ b/pkg/helm/helm_test.go @@ -13,7 +13,7 @@ import ( func TestRegisterTools(t *testing.T) { s := server.NewMCPServer("test-server", "v0.0.1") - RegisterTools(s) + RegisterTools(s, false) // false = enable all tools including write operations } // Test Helm List Releases diff --git a/pkg/istio/istio.go b/pkg/istio/istio.go index 680d83ca..dd1958c9 100644 --- a/pkg/istio/istio.go +++ b/pkg/istio/istio.go @@ -298,7 +298,8 @@ func handleZtunnelConfig(ctx context.Context, request mcp.CallToolRequest) (*mcp } // Register Istio tools -func RegisterTools(s *server.MCPServer) { +func RegisterTools(s *server.MCPServer, readOnly bool) { + // Read-only tools - always registered // Istio proxy status s.AddTool(mcp.NewTool("istio_proxy_status", @@ -315,13 +316,7 @@ func RegisterTools(s *server.MCPServer) { mcp.WithString("config_type", mcp.Description("Type of configuration (all, bootstrap, cluster, ecds, listener, log, route, secret)")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_proxy_config", handleIstioProxyConfig))) - // Istio install - s.AddTool(mcp.NewTool("istio_install_istio", - mcp.WithDescription("Install Istio with a specified configuration profile"), - mcp.WithString("profile", mcp.Description("Istio configuration profile (ambient, default, demo, minimal, empty)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_install_istio", handleIstioInstall))) - - // Istio generate manifest + // Istio generate manifest (read-only - just generates YAML, doesn't apply) s.AddTool(mcp.NewTool("istio_generate_manifest", mcp.WithDescription("Generate Istio manifest for a given profile"), mcp.WithString("profile", mcp.Description("Istio configuration profile (ambient, default, demo, minimal, empty)")), @@ -347,21 +342,11 @@ func RegisterTools(s *server.MCPServer) { mcp.WithDescription("List all waypoints in the mesh"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_list_waypoints", handleWaypointList))) - // Waypoint generate + // Waypoint generate (read-only - just generates YAML, doesn't apply) s.AddTool(mcp.NewTool("istio_generate_waypoint", mcp.WithDescription("Generate a waypoint resource YAML"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_generate_waypoint", handleWaypointGenerate))) - // Waypoint apply - s.AddTool(mcp.NewTool("istio_apply_waypoint", - mcp.WithDescription("Apply a waypoint resource to the cluster"), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_apply_waypoint", handleWaypointApply))) - - // Waypoint delete - s.AddTool(mcp.NewTool("istio_delete_waypoint", - mcp.WithDescription("Delete a waypoint resource from the cluster"), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_delete_waypoint", handleWaypointDelete))) - // Waypoint status s.AddTool(mcp.NewTool("istio_waypoint_status", mcp.WithDescription("Get the status of a waypoint resource"), @@ -371,4 +356,23 @@ func RegisterTools(s *server.MCPServer) { s.AddTool(mcp.NewTool("istio_ztunnel_config", mcp.WithDescription("Get the ztunnel configuration for a namespace"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_ztunnel_config", handleZtunnelConfig))) + + // Write tools - only registered when write operations are enabled + if !readOnly { + // Istio install + s.AddTool(mcp.NewTool("istio_install_istio", + mcp.WithDescription("Install Istio with a specified configuration profile"), + mcp.WithString("profile", mcp.Description("Istio configuration profile (ambient, default, demo, minimal, empty)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_install_istio", handleIstioInstall))) + + // Waypoint apply + s.AddTool(mcp.NewTool("istio_apply_waypoint", + mcp.WithDescription("Apply a waypoint resource to the cluster"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_apply_waypoint", handleWaypointApply))) + + // Waypoint delete + s.AddTool(mcp.NewTool("istio_delete_waypoint", + mcp.WithDescription("Delete a waypoint resource from the cluster"), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("istio_delete_waypoint", handleWaypointDelete))) + } } diff --git a/pkg/istio/istio_test.go b/pkg/istio/istio_test.go index d2503c9d..36abeef9 100644 --- a/pkg/istio/istio_test.go +++ b/pkg/istio/istio_test.go @@ -13,7 +13,7 @@ import ( func TestRegisterTools(t *testing.T) { s := server.NewMCPServer("test-server", "v0.0.1") - RegisterTools(s) + RegisterTools(s, false) // false = enable all tools including write operations } func TestHandleIstioProxyStatus(t *testing.T) { diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index c8e90852..f9184d12 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -558,9 +558,10 @@ func (k *K8sTool) runKubectlCommandWithTimeout(ctx context.Context, timeout time } // RegisterK8sTools registers all k8s tools with the MCP server -func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { +func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string, readOnly bool) { k8sTool := NewK8sToolWithConfig(kubeconfig, llm) + // Read-only tools - always registered s.AddTool(mcp.NewTool("k8s_get_resources", mcp.WithDescription("Get Kubernetes resources using kubectl"), mcp.WithString("resource_type", mcp.Description("Type of resource (pod, service, deployment, etc.)"), mcp.Required()), @@ -578,52 +579,11 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { mcp.WithNumber("tail_lines", mcp.Description("Number of lines to show from the end (default: 50)")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_pod_logs", k8sTool.handleKubectlLogsEnhanced))) - s.AddTool(mcp.NewTool("k8s_scale", - mcp.WithDescription("Scale a Kubernetes deployment"), - mcp.WithString("name", mcp.Description("Name of the deployment"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace of the deployment (default: default)")), - mcp.WithNumber("replicas", mcp.Description("Number of replicas"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_scale", k8sTool.handleScaleDeployment))) - - s.AddTool(mcp.NewTool("k8s_patch_resource", - mcp.WithDescription("Patch a Kubernetes resource using strategic merge patch"), - mcp.WithString("resource_type", mcp.Description("Type of resource (deployment, service, etc.)"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), - mcp.WithString("patch", mcp.Description("JSON patch to apply"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_patch_resource", k8sTool.handlePatchResource))) - - s.AddTool(mcp.NewTool("k8s_apply_manifest", - mcp.WithDescription("Apply a YAML manifest to the Kubernetes cluster"), - mcp.WithString("manifest", mcp.Description("YAML manifest content"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_apply_manifest", k8sTool.handleApplyManifest))) - - s.AddTool(mcp.NewTool("k8s_delete_resource", - mcp.WithDescription("Delete a Kubernetes resource"), - mcp.WithString("resource_type", mcp.Description("Type of resource (pod, service, deployment, etc.)"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_delete_resource", k8sTool.handleDeleteResource))) - - s.AddTool(mcp.NewTool("k8s_check_service_connectivity", - mcp.WithDescription("Check connectivity to a service using a temporary curl pod"), - mcp.WithString("service_name", mcp.Description("Service name to test (e.g., my-service.my-namespace.svc.cluster.local:80)"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace to run the check from (default: default)")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_check_service_connectivity", k8sTool.handleCheckServiceConnectivity))) - s.AddTool(mcp.NewTool("k8s_get_events", mcp.WithDescription("Get events from a Kubernetes namespace"), mcp.WithString("namespace", mcp.Description("Namespace to get events from (default: default)")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_events", k8sTool.handleGetEvents))) - s.AddTool(mcp.NewTool("k8s_execute_command", - mcp.WithDescription("Execute a command in a Kubernetes pod"), - mcp.WithString("pod_name", mcp.Description("Name of the pod to execute in"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace of the pod (default: default)")), - mcp.WithString("container", mcp.Description("Container name (for multi-container pods)")), - mcp.WithString("command", mcp.Description("Command to execute"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_execute_command", k8sTool.handleExecCommand))) - s.AddTool(mcp.NewTool("k8s_get_available_api_resources", mcp.WithDescription("Get available Kubernetes API resources"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_available_api_resources", k8sTool.handleGetAvailableAPIResources))) @@ -632,82 +592,6 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { mcp.WithDescription("Get cluster configuration details"), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_get_cluster_configuration", k8sTool.handleGetClusterConfiguration))) - s.AddTool(mcp.NewTool("k8s_rollout", - mcp.WithDescription("Perform rollout operations on Kubernetes resources (history, pause, restart, resume, status, undo)"), - mcp.WithString("action", mcp.Description("The rollout action to perform"), mcp.Required()), - mcp.WithString("resource_type", mcp.Description("The type of resource to rollout (e.g., deployment)"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("The name of the resource to rollout"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_rollout", k8sTool.handleRollout))) - - s.AddTool(mcp.NewTool("k8s_label_resource", - mcp.WithDescription("Add or update labels on a Kubernetes resource"), - mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), - mcp.WithString("labels", mcp.Description("Space-separated key=value pairs for labels"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_label_resource", k8sTool.handleLabelResource))) - - s.AddTool(mcp.NewTool("k8s_annotate_resource", - mcp.WithDescription("Add or update annotations on a Kubernetes resource"), - mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), - mcp.WithString("annotations", mcp.Description("Space-separated key=value pairs for annotations"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_annotate_resource", k8sTool.handleAnnotateResource))) - - s.AddTool(mcp.NewTool("k8s_remove_annotation", - mcp.WithDescription("Remove an annotation from a Kubernetes resource"), - mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), - mcp.WithString("annotation_key", mcp.Description("The key of the annotation to remove"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_remove_annotation", k8sTool.handleRemoveAnnotation))) - - s.AddTool(mcp.NewTool("k8s_remove_label", - mcp.WithDescription("Remove a label from a Kubernetes resource"), - mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), - mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), - mcp.WithString("label_key", mcp.Description("The key of the label to remove"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace of the resource")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_remove_label", k8sTool.handleRemoveLabel))) - - s.AddTool(mcp.NewTool("k8s_create_resource", - mcp.WithDescription("Create a Kubernetes resource from YAML content"), - mcp.WithString("yaml_content", mcp.Description("YAML content of the resource"), mcp.Required()), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_create_resource", func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - yamlContent := mcp.ParseString(request, "yaml_content", "") - - if yamlContent == "" { - return mcp.NewToolResultError("yaml_content is required"), nil - } - - // Create temporary file - tmpFile, err := os.CreateTemp("", "k8s-resource-*.yaml") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create temp file: %v", err)), nil - } - defer os.Remove(tmpFile.Name()) - - if _, err := tmpFile.WriteString(yamlContent); err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to write to temp file: %v", err)), nil - } - tmpFile.Close() - - result, err := k8sTool.runKubectlCommand(ctx, "create", "-f", tmpFile.Name()) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Create command failed: %v", err)), nil - } - - return result, nil - }))) - - s.AddTool(mcp.NewTool("k8s_create_resource_from_url", - mcp.WithDescription("Create a Kubernetes resource from a URL pointing to a YAML manifest"), - mcp.WithString("url", mcp.Description("The URL of the manifest"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("The namespace to create the resource in")), - ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_create_resource_from_url", k8sTool.handleCreateResourceFromURL))) - s.AddTool(mcp.NewTool("k8s_get_resource_yaml", mcp.WithDescription("Get the YAML representation of a Kubernetes resource"), mcp.WithString("resource_type", mcp.Description("Type of resource"), mcp.Required()), @@ -747,4 +631,124 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) { mcp.WithString("resource_description", mcp.Description("Detailed description of the resource to generate"), mcp.Required()), mcp.WithString("resource_type", mcp.Description(fmt.Sprintf("Type of resource to generate (%s)", strings.Join(slices.Collect(resourceTypes), ", "))), mcp.Required()), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_generate_resource", k8sTool.handleGenerateResource))) + + // Write tools - only registered when write operations are enabled + if !readOnly { + s.AddTool(mcp.NewTool("k8s_scale", + mcp.WithDescription("Scale a Kubernetes deployment"), + mcp.WithString("name", mcp.Description("Name of the deployment"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace of the deployment (default: default)")), + mcp.WithNumber("replicas", mcp.Description("Number of replicas"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_scale", k8sTool.handleScaleDeployment))) + + s.AddTool(mcp.NewTool("k8s_patch_resource", + mcp.WithDescription("Patch a Kubernetes resource using strategic merge patch"), + mcp.WithString("resource_type", mcp.Description("Type of resource (deployment, service, etc.)"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), + mcp.WithString("patch", mcp.Description("JSON patch to apply"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_patch_resource", k8sTool.handlePatchResource))) + + s.AddTool(mcp.NewTool("k8s_apply_manifest", + mcp.WithDescription("Apply a YAML manifest to the Kubernetes cluster"), + mcp.WithString("manifest", mcp.Description("YAML manifest content"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_apply_manifest", k8sTool.handleApplyManifest))) + + s.AddTool(mcp.NewTool("k8s_delete_resource", + mcp.WithDescription("Delete a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("Type of resource (pod, service, deployment, etc.)"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_delete_resource", k8sTool.handleDeleteResource))) + + s.AddTool(mcp.NewTool("k8s_check_service_connectivity", + mcp.WithDescription("Check connectivity to a service using a temporary curl pod"), + mcp.WithString("service_name", mcp.Description("Service name to test (e.g., my-service.my-namespace.svc.cluster.local:80)"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace to run the check from (default: default)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_check_service_connectivity", k8sTool.handleCheckServiceConnectivity))) + + s.AddTool(mcp.NewTool("k8s_execute_command", + mcp.WithDescription("Execute a command in a Kubernetes pod"), + mcp.WithString("pod_name", mcp.Description("Name of the pod to execute in"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace of the pod (default: default)")), + mcp.WithString("container", mcp.Description("Container name (for multi-container pods)")), + mcp.WithString("command", mcp.Description("Command to execute"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_execute_command", k8sTool.handleExecCommand))) + + s.AddTool(mcp.NewTool("k8s_rollout", + mcp.WithDescription("Perform rollout operations on Kubernetes resources (history, pause, restart, resume, status, undo)"), + mcp.WithString("action", mcp.Description("The rollout action to perform"), mcp.Required()), + mcp.WithString("resource_type", mcp.Description("The type of resource to rollout (e.g., deployment)"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("The name of the resource to rollout"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the resource")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_rollout", k8sTool.handleRollout))) + + s.AddTool(mcp.NewTool("k8s_label_resource", + mcp.WithDescription("Add or update labels on a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), + mcp.WithString("labels", mcp.Description("Space-separated key=value pairs for labels"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the resource")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_label_resource", k8sTool.handleLabelResource))) + + s.AddTool(mcp.NewTool("k8s_annotate_resource", + mcp.WithDescription("Add or update annotations on a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), + mcp.WithString("annotations", mcp.Description("Space-separated key=value pairs for annotations"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the resource")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_annotate_resource", k8sTool.handleAnnotateResource))) + + s.AddTool(mcp.NewTool("k8s_remove_annotation", + mcp.WithDescription("Remove an annotation from a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), + mcp.WithString("annotation_key", mcp.Description("The key of the annotation to remove"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the resource")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_remove_annotation", k8sTool.handleRemoveAnnotation))) + + s.AddTool(mcp.NewTool("k8s_remove_label", + mcp.WithDescription("Remove a label from a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("The type of resource"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("The name of the resource"), mcp.Required()), + mcp.WithString("label_key", mcp.Description("The key of the label to remove"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace of the resource")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_remove_label", k8sTool.handleRemoveLabel))) + + s.AddTool(mcp.NewTool("k8s_create_resource", + mcp.WithDescription("Create a Kubernetes resource from YAML content"), + mcp.WithString("yaml_content", mcp.Description("YAML content of the resource"), mcp.Required()), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_create_resource", func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + yamlContent := mcp.ParseString(request, "yaml_content", "") + + if yamlContent == "" { + return mcp.NewToolResultError("yaml_content is required"), nil + } + + // Create temporary file + tmpFile, err := os.CreateTemp("", "k8s-resource-*.yaml") + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to create temp file: %v", err)), nil + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.WriteString(yamlContent); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Failed to write to temp file: %v", err)), nil + } + tmpFile.Close() + + result, err := k8sTool.runKubectlCommand(ctx, "create", "-f", tmpFile.Name()) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Create command failed: %v", err)), nil + } + + return result, nil + }))) + + s.AddTool(mcp.NewTool("k8s_create_resource_from_url", + mcp.WithDescription("Create a Kubernetes resource from a URL pointing to a YAML manifest"), + mcp.WithString("url", mcp.Description("The URL of the manifest"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("The namespace to create the resource in")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_create_resource_from_url", k8sTool.handleCreateResourceFromURL))) + } } diff --git a/pkg/kubescape/kubescape.go b/pkg/kubescape/kubescape.go index 8893da30..e2227fa4 100644 --- a/pkg/kubescape/kubescape.go +++ b/pkg/kubescape/kubescape.go @@ -1047,7 +1047,7 @@ func truncateString(s string, maxLen int) string { } // RegisterTools registers all Kubescape tools with the MCP server -func RegisterTools(s *server.MCPServer, kubeconfig string) { +func RegisterTools(s *server.MCPServer, kubeconfig string, readOnly bool) { tool := NewKubescapeTool(kubeconfig) // Health check tool diff --git a/pkg/kubescape/kubescape_test.go b/pkg/kubescape/kubescape_test.go index 6340b8d5..bd3ac324 100644 --- a/pkg/kubescape/kubescape_test.go +++ b/pkg/kubescape/kubescape_test.go @@ -42,7 +42,7 @@ func TestRegisterTools(t *testing.T) { // Should not panic assert.NotPanics(t, func() { - RegisterTools(s, "") + RegisterTools(s, "", false) }) // Verify tools are registered by checking the server has tools diff --git a/pkg/prometheus/prometheus.go b/pkg/prometheus/prometheus.go index 1239305f..c77e23d4 100644 --- a/pkg/prometheus/prometheus.go +++ b/pkg/prometheus/prometheus.go @@ -303,7 +303,7 @@ func handlePrometheusTargetsQueryTool(ctx context.Context, request mcp.CallToolR return mcp.NewToolResultText(string(prettyJSON)), nil } -func RegisterTools(s *server.MCPServer) { +func RegisterTools(s *server.MCPServer, readOnly bool) { s.AddTool(mcp.NewTool("prometheus_query_tool", mcp.WithDescription("Execute a PromQL query against Prometheus"), mcp.WithString("query", mcp.Description("PromQL query to execute"), mcp.Required()), diff --git a/pkg/utils/common.go b/pkg/utils/common.go index ce8b73bf..23070889 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -74,27 +74,29 @@ func handleGetCurrentDateTimeTool(ctx context.Context, request mcp.CallToolReque return mcp.NewToolResultText(now.Format(time.RFC3339)), nil } -func RegisterTools(s *server.MCPServer) { +func RegisterTools(s *server.MCPServer, readOnly bool) { logger.Get().Info("RegisterTools initialized") - // Register shell tool - s.AddTool(mcp.NewTool("shell", - mcp.WithDescription("Execute shell commands"), - mcp.WithString("command", mcp.Description("The shell command to execute"), mcp.Required()), - ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - command := mcp.ParseString(request, "command", "") - if command == "" { - return mcp.NewToolResultError("command parameter is required"), nil - } - - params := shellParams{Command: command} - result, err := shellTool(ctx, params) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - return mcp.NewToolResultText(result), nil - }) + // Register shell tool - disabled in read-only mode as it allows arbitrary command execution + if !readOnly { + s.AddTool(mcp.NewTool("shell", + mcp.WithDescription("Execute shell commands"), + mcp.WithString("command", mcp.Description("The shell command to execute"), mcp.Required()), + ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + command := mcp.ParseString(request, "command", "") + if command == "" { + return mcp.NewToolResultError("command parameter is required"), nil + } + + params := shellParams{Command: command} + result, err := shellTool(ctx, params) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + return mcp.NewToolResultText(result), nil + }) + } // Register datetime tool s.AddTool(mcp.NewTool("datetime_get_current_time", From 1ed98d3f79f77e2092d311375fe42e620bde7de5 Mon Sep 17 00:00:00 2001 From: Matteo Mori <169821341+MatteoMori8@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:52:17 +0000 Subject: [PATCH 30/41] fix(deps): patch HIGH security vulnerabilities in Go dependencies (#44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade all Go dependencies to latest versions and bump bundled CLI tools (kubectl 1.35.1, helm 4.1.1) to address HIGH severity vulnerabilities flagged by security scanning. Pin kubescape/storage to v0.0.239 (latest compatible release) as v0.2.0 removed APIs we depend on. 8 remaining HIGHs cannot be addressed as they originate from upstream pre-compiled binaries (istioctl 1.28.3, kubectl-argo-rollouts 1.8.3) which are already at their latest releases: ✅ TOOLS_ARGO_ROLLOUTS_VERSION=1.8.3 == v1.8.3 ✅ TOOLS_CILIUM_VERSION=0.19.0 == v0.19.0 ✅ TOOLS_ISTIO_VERSION=1.28.3 == 1.28.3 ❌ TOOLS_HELM_VERSION=4.1.0 != v4.1.1 (bumped) ❌ TOOLS_KUBECTL_VERSION=1.35.0 != v1.35.1 (bumped) Signed-off-by: Matteo Mori --- Makefile | 4 +- go.mod | 175 ++++++++------- go.sum | 384 +++++++++++++++++--------------- pkg/kubescape/kubescape_test.go | 78 +++---- 4 files changed, 337 insertions(+), 304 deletions(-) diff --git a/Makefile b/Makefile index 892c7a17..4b188f15 100644 --- a/Makefile +++ b/Makefile @@ -138,8 +138,8 @@ DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUI # tools image build args TOOLS_ISTIO_VERSION ?= 1.28.3 TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 -TOOLS_KUBECTL_VERSION ?= 1.35.0 -TOOLS_HELM_VERSION ?= 4.1.0 +TOOLS_KUBECTL_VERSION ?= 1.35.1 +TOOLS_HELM_VERSION ?= 4.1.1 TOOLS_CILIUM_VERSION ?= 0.19.0 # build args diff --git a/go.mod b/go.mod index 3f7722c6..a2f27fc0 100644 --- a/go.mod +++ b/go.mod @@ -4,25 +4,25 @@ go 1.25.6 require ( github.com/joho/godotenv v1.5.1 - github.com/kubescape/k8s-interface v0.0.200 - github.com/kubescape/storage v0.0.238 + github.com/kubescape/k8s-interface v0.0.203 + github.com/kubescape/storage v0.0.239 github.com/mark3labs/mcp-go v0.43.2 github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/tmc/langchaingo v0.1.14 - go.opentelemetry.io/otel v1.39.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 - go.opentelemetry.io/otel/metric v1.39.0 - go.opentelemetry.io/otel/sdk v1.38.0 - go.opentelemetry.io/otel/trace v1.39.0 - k8s.io/api v0.35.0 - k8s.io/apiextensions-apiserver v0.35.0 - k8s.io/apimachinery v0.35.0 - k8s.io/client-go v0.35.0 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 + go.opentelemetry.io/otel/metric v1.40.0 + go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 + k8s.io/api v0.35.1 + k8s.io/apiextensions-apiserver v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 ) require ( @@ -30,13 +30,12 @@ require ( github.com/acobaugh/osrelease v0.1.0 // indirect github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 // indirect github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 // indirect - github.com/anchore/stereoscope v0.1.9-0.20250826202322-ef061ea78385 // indirect + github.com/anchore/stereoscope v0.1.9 // indirect github.com/anchore/syft v1.32.0 // indirect - github.com/armosec/armoapi-go v0.0.596 // indirect + github.com/armosec/armoapi-go v0.0.674 // indirect github.com/armosec/gojay v1.2.17 // indirect github.com/armosec/utils-go v0.0.58 // indirect - github.com/armosec/utils-k8s-go v0.0.30 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/armosec/utils-k8s-go v0.0.35 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -47,57 +46,70 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cilium/cilium v1.16.17 // indirect + github.com/cilium/cilium v1.19.0 // indirect + github.com/cilium/ebpf v0.20.1-0.20260108141042-f7e80f49188b // indirect + github.com/cilium/hive v0.0.1 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containers/common v0.63.0 // indirect - github.com/coreos/go-oidc/v3 v3.14.1 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dlclark/regexp2 v1.10.0 // indirect - github.com/docker/cli v28.3.2+incompatible // indirect - github.com/docker/docker v28.3.3+incompatible // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/docker/cli v28.3.3+incompatible // indirect + github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/docker/go-connections v0.6.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/facebookincubator/nvdtools v0.1.5 // indirect github.com/fatih/color v1.18.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/github/go-spdx/v2 v2.3.3 // indirect + github.com/gkampitakis/go-snaps v0.5.19 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.1 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.23.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-openapi/analysis v0.24.2 // indirect + github.com/go-openapi/errors v0.22.6 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/loads v0.23.2 // indirect + github.com/go-openapi/spec v0.22.3 // indirect + github.com/go-openapi/strfmt v0.25.0 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-openapi/validate v0.25.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/gohugoio/hashstructure v0.5.0 // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-containerregistry v0.20.6 // indirect github.com/google/licensecheck v0.3.1 // indirect github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/jinzhu/copier v0.4.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/kubescape/go-logger v0.0.26 // indirect - github.com/mackerelio/go-osstat v0.2.5 // indirect + github.com/mackerelio/go-osstat v0.2.6 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -111,26 +123,25 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.2.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.8 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sasha-s/go-deadlock v0.3.5 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/sasha-s/go-deadlock v0.3.6 // indirect github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect github.com/seccomp/libseccomp-golang v0.10.0 // indirect github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.14.0 // indirect + github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.20.1 // indirect + github.com/spf13/viper v1.21.0 // indirect github.com/stripe/stripe-go/v74 v74.30.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/sylabs/squashfs v1.0.6 // indirect @@ -138,8 +149,8 @@ require ( github.com/ulikunitz/xz v0.5.15 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2 // indirect - github.com/uptrace/uptrace-go v1.37.0 // indirect - github.com/vishvananda/netlink v1.3.1-0.20241022031324-976bd8de7d81 // indirect + github.com/uptrace/uptrace-go v1.39.0 // indirect + github.com/vishvananda/netlink v1.3.2-0.20260109214200-c6faf428e8f8 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 // indirect @@ -147,47 +158,47 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/yl2chen/cidranger v1.0.2 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - go.mongodb.org/mongo-driver v1.17.1 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 // indirect - go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect - go.opentelemetry.io/otel/log v0.15.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.8.0 // indirect + go.opentelemetry.io/contrib/bridges/otelslog v0.15.0 // indirect + go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/log v0.16.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/dig v1.19.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.32.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect - google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiserver v0.35.0 // indirect - k8s.io/component-base v0.35.0 // indirect + k8s.io/apiserver v0.35.1 // indirect + k8s.io/component-base v0.35.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect - sigs.k8s.io/controller-runtime v0.20.4 // indirect + k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + sigs.k8s.io/controller-runtime v0.23.1 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 58327a02..2411c567 100644 --- a/go.sum +++ b/go.sum @@ -70,18 +70,18 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716 h1:2sIdYJlQESEnyk3Y0WD2vXWW5eD2iMz9Ev8fj1Z8LNA= -github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716/go.mod h1:Utb9i4kwiCWvqAIxZaJeMIXFO9uOgQXlvH2BfbfO/zI= -github.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe h1:qv/xxpjF5RdKPqZjx8RM0aBi3HUCAO0DhRBMs2xhY1I= -github.com/anchore/fangs v0.0.0-20250402135612-96e29e45f3fe/go.mod h1:vrcYMDps9YXwwx2a9AsvipM6Fi5H9//9bymGb8G8BIQ= +github.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084 h1:7DUAXEdAxoANPlDgxYiaSRKnWnTygvdrrWhnmvEjNLg= +github.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084/go.mod h1:42dWox8z4//b898OIELsQnSdYq9q1aCXkwp5fKF+BEU= +github.com/anchore/fangs v0.0.0-20250716230140-94c22408c232 h1:aVC6r9h5wGNh8BYTW3CXxOdPoZzY/bBRWne1NvSTlO8= +github.com/anchore/fangs v0.0.0-20250716230140-94c22408c232/go.mod h1:Zees1AEKNpXIRgdVAMYWITncarLFiPOtEQ7rl45V/h0= github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc= github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50= github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM= github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI= -github.com/anchore/stereoscope v0.1.9-0.20250826202322-ef061ea78385 h1:icCqbvAKGZXf29lEi8JmwvHVCBCYkiyZMuSnk+5ajYo= -github.com/anchore/stereoscope v0.1.9-0.20250826202322-ef061ea78385/go.mod h1:0UCjLz5MdPNiH9F0h2tSNf3yGF6/MnK8ZCPo0YfDQVc= +github.com/anchore/stereoscope v0.1.9 h1:Nhvk8g6PRx9ubaJU4asAhD3fGcY5HKXZCDGkxI2e0sI= +github.com/anchore/stereoscope v0.1.9/go.mod h1:YkrCtDgz7A+w6Ggd0yxU9q58CerqQFwYARS+F2RvLQQ= github.com/anchore/syft v1.32.0 h1:JcX9W+P/Xjv5DNg3TNBtwiEyZommuTaP16/NC9r0Yfo= github.com/anchore/syft v1.32.0/go.mod h1:E6Kd4iBM2ljUOUQvSt7hVK6vBwaHkMXwcvBZmGMSY5o= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= @@ -92,16 +92,14 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armosec/armoapi-go v0.0.596 h1:n8xB6Y/zuzjAqqwc7zJPXxdvn6pqZK94IC6x7nvj1oI= -github.com/armosec/armoapi-go v0.0.596/go.mod h1:GQQzRuP8OBvbDx7GGwOyw3TCjk5NtK3WbeyfuLoiEts= +github.com/armosec/armoapi-go v0.0.674 h1:jsp4rZqs+iKeL/Y4GgXW6JkVf0DhrilhaqRq3bar8HY= +github.com/armosec/armoapi-go v0.0.674/go.mod h1:9jAH0g8ZsryhiBDd/aNMX4+n10bGwTx/doWCyyjSxts= github.com/armosec/gojay v1.2.17 h1:VSkLBQzD1c2V+FMtlGFKqWXNsdNvIKygTKJI9ysY8eM= github.com/armosec/gojay v1.2.17/go.mod h1:vuvX3DlY0nbVrJ0qCklSS733AWMoQboq3cFyuQW9ybc= github.com/armosec/utils-go v0.0.58 h1:g9RnRkxZAmzTfPe2ruMo2OXSYLwVSegQSkSavOfmaIE= github.com/armosec/utils-go v0.0.58/go.mod h1:CdqKHKruVJMCxGcZXYW9J+5P9FZou8dMzVpcB0Xt8pk= -github.com/armosec/utils-k8s-go v0.0.30 h1:Gj8MJck0jZPSLSq8ZMiRPT3F/laOYQdaLxXKKcjijt4= -github.com/armosec/utils-k8s-go v0.0.30/go.mod h1:t0vvPJhYE+X+bOsaMsD2SzWU7WkJmV2Ltn9hg66AIe8= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/armosec/utils-k8s-go v0.0.35 h1:CliNObhAca5UYl84m5OQecOTm9ZfMFI8648pYhQJiu4= +github.com/armosec/utils-k8s-go v0.0.35/go.mod h1:iHwR/KhMFtdd8Px1oYexLZYOHqmdknfGTZ8b7sZS0Ms= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= @@ -135,8 +133,12 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/cilium v1.16.17 h1:5DUFyl/DhCEhWGdMQjw0eA9FlI8dBFpg2ENYJan3Wk0= -github.com/cilium/cilium v1.16.17/go.mod h1:Wa47utg/8XuOe8pq64KwNOM8wDsXoYP3HZ6uw3IYDVg= +github.com/cilium/cilium v1.19.0 h1:hfPhb9TcoG3fRMA1/ExBFihEX1NpEWcUaV1jXJTkQVo= +github.com/cilium/cilium v1.19.0/go.mod h1:yDDJNQbgFXDFdaVWlAWD8M3n8UZwHUk8Bo8JGgllx7o= +github.com/cilium/ebpf v0.20.1-0.20260108141042-f7e80f49188b h1:ubt7adiPfM2/6QrjNI8T+LAe4J7KHVkAMbEJ3+LLTXk= +github.com/cilium/ebpf v0.20.1-0.20260108141042-f7e80f49188b/go.mod h1:dM+AMI6FkW5LOkzikdefUmzK0z81o7GqiKXon7D1F58= +github.com/cilium/hive v0.0.1 h1:NrHJ1DD74B77ib4UhujEQ0j4nCQmyKOic9qtwrreJRs= +github.com/cilium/hive v0.0.1/go.mod h1:4/8FBMcTjVdkrNNWaB7t3QqaU4kZDJLJ1leKVP9GjEI= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -154,8 +156,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containers/common v0.63.0 h1:ox6vgUYX5TSvt4W+bE36sYBVz/aXMAfRGVAgvknSjBg= github.com/containers/common v0.63.0/go.mod h1:+3GCotSqNdIqM3sPs152VvW7m5+Mg8Kk+PExT3G9hZw= -github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= -github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -166,19 +168,19 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY= -github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= -github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo= +github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -213,17 +215,15 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/github/go-spdx/v2 v2.3.3 h1:QI7evnHWEfWkT54eJwkoV/f3a0xD3gLlnVmT5wQG6LE= github.com/github/go-spdx/v2 v2.3.3/go.mod h1:2ZxKsOhvBp+OYBDlsGnUMcchLeo2mrpEBn2L1C+U3IQ= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= -github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= -github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= -github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= -github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/gkampitakis/go-snaps v0.5.19 h1:hUJlCQOpTt1M+kSisMwioDWZDWpDtdAvUhvWCx1YGW0= +github.com/gkampitakis/go-snaps v0.5.19/go.mod h1:gC3YqxQTPyIXvQrw/Vpt3a8VqR1MO8sVpZFWN4DGwNs= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -242,37 +242,66 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= -github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-openapi/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50= +github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE= +github.com/go-openapi/errors v0.22.6 h1:eDxcf89O8odEnohIXwEjY1IB4ph5vmbUsBMsFNwXWPo= +github.com/go-openapi/errors v0.22.6/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= +github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= +github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= +github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= +github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= +github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg= github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec= @@ -313,8 +342,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -367,14 +396,14 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= -github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= -github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= +github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8 h1:NpbJl/eVbvrGE0MJ6X16X9SAifesl6Fwxg/YmCvubRI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8/go.mod h1:mi7YA+gCzVem12exXy46ZespvGtX/lZmD/RLnQhVW7U= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -424,8 +453,6 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -438,8 +465,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -454,16 +481,16 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubescape/go-logger v0.0.26 h1:pOADnCLaooXwa7XUzmelCasq8h/vWhjHScJITuKoTUk= github.com/kubescape/go-logger v0.0.26/go.mod h1:qfUg4BGH2Rbxy+Tn3g4ks2Lt7zOENKA8AMXnAfkZpl0= -github.com/kubescape/k8s-interface v0.0.200 h1:Ff64dlDigg8dDYJuaeLFFjfTCHQNC1SStWNECWFRCYE= -github.com/kubescape/k8s-interface v0.0.200/go.mod h1:j9snZbH+RxOaa1yG/bWgTClj90q7To0rGgQepxy4b+k= -github.com/kubescape/storage v0.0.238 h1:4PNM/6RZSNTgLddHNsSId/58CpZQw8uHQ0KhOakHU0Y= -github.com/kubescape/storage v0.0.238/go.mod h1:TGlwvK5ixF6Zfm/qyeRw10D0DyN43QFU5VUZmYfm80k= +github.com/kubescape/k8s-interface v0.0.203 h1:dMlF+X8PQPUhcn9QeMipnHd/6/gkOvyC0smLz1uBO5I= +github.com/kubescape/k8s-interface v0.0.203/go.mod h1:d4NVhL81bVXe8yEXlkT4ZHrt3iEppEIN39b8N1oXm5s= +github.com/kubescape/storage v0.0.239 h1:hfuq1+CuEAKE7zCg9bB8gfU9vZoGMrJBgNh5tAD1rak= +github.com/kubescape/storage v0.0.239/go.mod h1:f6u/Lt3SjUTBrmzOStb33IkKTtaqKM4pyfV5d1lUMiY= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= -github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o= -github.com/mackerelio/go-osstat v0.2.5/go.mod h1:atxwWF+POUZcdtR1wnsUcQxTytoHG4uhl2AKKzrOajY= +github.com/mackerelio/go-osstat v0.2.6 h1:gs4U8BZeS1tjrL08tt5VUliVvSWP26Ai2Ob8Lr7f2i0= +github.com/mackerelio/go-osstat v0.2.6/go.mod h1:lRy8V9ZuHpuRVZh+vyTkODeDPl3/d5MgXHtLSaqG8bA= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= @@ -542,11 +569,11 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 h1:ah1dvbqPMN5+ocrg/ZSgZ6k8bOk+kcZQ7fnyx6UvOm4= -github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -578,14 +605,14 @@ github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvM github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -594,10 +621,10 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= -github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/sasha-s/go-deadlock v0.3.6 h1:TR7sfOnZ7x00tWPfD397Peodt57KzMDo+9Ae9rMiUmw= +github.com/sasha-s/go-deadlock v0.3.6/go.mod h1:CUqNyyvMxTyjFqDT7MRg9mb4Dv/btmGTqSR+rky/UXo= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -635,14 +662,12 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= -github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= @@ -655,8 +680,8 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -698,13 +723,12 @@ github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2 h1:3/aHKUq7qaFMWxyQV0W github.com/uptrace/opentelemetry-go-extra/otelutil v0.3.2/go.mod h1:Zit4b8AQXaXvA68+nzmbyDzqiyFRISyw1JiD5JqUBjw= github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2 h1:cj/Z6FKTTYBnstI0Lni9PA+k2foounKIPUmj1LBwNiQ= github.com/uptrace/opentelemetry-go-extra/otelzap v0.3.2/go.mod h1:LDaXk90gKEC2nC7JH3Lpnhfu+2V7o/TsqomJJmqA39o= -github.com/uptrace/uptrace-go v1.37.0 h1:9ohbWB0qZEfcPLFbfqAAt5wz2rcBmL60/QqkOkvqYOs= -github.com/uptrace/uptrace-go v1.37.0/go.mod h1:3xAdXLVyEoqvRwuj3D/n1s9bLl7Ok+OnNaW889fvtDQ= +github.com/uptrace/uptrace-go v1.39.0 h1:MszuE3eX/z86xzYywN2JBtYcmsS4ofdo1VMDhRvkWrI= +github.com/uptrace/uptrace-go v1.39.0/go.mod h1:FquipEqgTMXPbhdhenjbiLHG1R5WYdxVH6zgwHeMzzA= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/vishvananda/netlink v1.3.1-0.20241022031324-976bd8de7d81 h1:9fkQcQYvtTr9ayFXuMfDMVuDt4+BYG9FwsGLnrBde0M= -github.com/vishvananda/netlink v1.3.1-0.20241022031324-976bd8de7d81/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= -github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vishvananda/netlink v1.3.2-0.20260109214200-c6faf428e8f8 h1:/EaCkwYyCH9rDgccb78ZTaGwo7UGjjdh0iyCa3+miRs= +github.com/vishvananda/netlink v1.3.2-0.20260109214200-c6faf428e8f8/go.mod h1:lEui7SPMd9fgxzHVGRAvTxsBGCF6PRH81o2kLWLWHgw= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= @@ -729,8 +753,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= -go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= -go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -741,50 +765,52 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM= -go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc= -go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 h1:ZIt0ya9/y4WyRIzfLC8hQRRsWg0J9M9GyaGtIMiElZI= -go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0/go.mod h1:F1aJ9VuiKWOlWwKdTYDUp1aoS0HzQxg38/VLxKmhm5U= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 h1:zUfYw8cscHHLwaY8Xz3fiJu+R59xBnkgq2Zr1lwmK/0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0/go.mod h1:514JLMCcFLQFS8cnTepOk6I09cKWJ5nGHBxHrMJ8Yfg= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0 h1:9PgnL3QNlj10uGxExowIDIZu66aVBwWhXmbOp1pa6RA= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.37.0/go.mod h1:0ineDcLELf6JmKfuo0wvvhAVMuxWFYvkTin2iV4ydPQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= -go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= -go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ= -go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw= -go.opentelemetry.io/otel/sdk/log/logtest v0.13.0 h1:9yio6AFZ3QD9j9oqshV1Ibm9gPLlHNxurno5BreMtIA= -go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/contrib/bridges/otelslog v0.15.0 h1:yOYhGNPZseueTTvWp5iBD3/CthrmvayUXYEX862dDi4= +go.opentelemetry.io/contrib/bridges/otelslog v0.15.0/go.mod h1:CvaNVqIfcybc+7xqZNubbE+26K6P7AKZF/l0lE2kdCk= +go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0 h1:n8qdwrebNEHF/zHpueuZ4OacdJ8CdSaP7xef9WRZXTQ= +go.opentelemetry.io/contrib/instrumentation/runtime v0.65.0/go.mod h1:Z1pjGxUL3nJ/IbDDfL6rBD0Xbz7ZOViRqrIUg4l1CYE= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= +go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= +go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= +go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= -go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -815,8 +841,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -844,8 +868,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -892,8 +916,8 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -913,8 +937,8 @@ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -927,8 +951,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -998,14 +1022,12 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1015,14 +1037,14 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1079,8 +1101,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1198,10 +1220,10 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -1232,8 +1254,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1248,8 +1270,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1284,35 +1306,35 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= -k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= -k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= -k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= -k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= -k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= -k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= -k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= -k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= -k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= -k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs= +k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= +k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= -sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= diff --git a/pkg/kubescape/kubescape_test.go b/pkg/kubescape/kubescape_test.go index bd3ac324..2b0bcafe 100644 --- a/pkg/kubescape/kubescape_test.go +++ b/pkg/kubescape/kubescape_test.go @@ -120,7 +120,7 @@ func TestHandleCheckHealth_AllComponentsHealthy(t *testing.T) { // NOTE: SBOM CRD check is disabled (SBOM tools are too large for LLM context) ) - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.VulnerabilityManifest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-manifest", @@ -182,7 +182,7 @@ func TestHandleCheckHealth_NamespaceNotFound(t *testing.T) { k8sClient := kubefake.NewSimpleClientset() // No namespace //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs apiExtClient := apiextensionsfake.NewSimpleClientset() - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) @@ -207,7 +207,7 @@ func TestHandleCheckHealth_OperatorPodsNotRunning(t *testing.T) { ) //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs apiExtClient := apiextensionsfake.NewSimpleClientset() - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) @@ -240,7 +240,7 @@ func TestHandleCheckHealth_OperatorPodsUnhealthy(t *testing.T) { ) //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs apiExtClient := apiextensionsfake.NewSimpleClientset() - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) @@ -264,7 +264,7 @@ func TestHandleCheckHealth_VulnerabilityCRDMissing(t *testing.T) { ) //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs apiExtClient := apiextensionsfake.NewSimpleClientset() // No CRDs - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) @@ -295,7 +295,7 @@ func TestHandleCheckHealth_NoScanData(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: workloadConfigurationScansCRD}, }, ) - spdxClient := kubescapefake.NewSimpleClientset() // No vulnerability manifests + spdxClient := kubescapefake.NewClientset() // No vulnerability manifests tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) @@ -327,7 +327,7 @@ func TestHandleCheckHealth_RuntimeObservabilityCRDsMissing(t *testing.T) { }, // No runtime observability CRDs (applicationprofiles, networkneighborhoods) ) - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) @@ -377,7 +377,7 @@ func TestHandleCheckHealth_CustomNamespace(t *testing.T) { ) //nolint:staticcheck // NewSimpleClientset is deprecated but NewClientset requires generated apply configs apiExtClient := apiextensionsfake.NewSimpleClientset() - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(k8sClient, apiExtClient, spdxClient.SpdxV1beta1()) @@ -405,7 +405,7 @@ func TestHandleCheckHealth_InitError(t *testing.T) { } func TestHandleListVulnerabilityManifests_Success(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.VulnerabilityManifest{ ObjectMeta: metav1.ObjectMeta{ Name: "manifest-1", @@ -448,7 +448,7 @@ func TestHandleListVulnerabilityManifests_Success(t *testing.T) { } func TestHandleListVulnerabilityManifests_FilterByNamespace(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.VulnerabilityManifest{ ObjectMeta: metav1.ObjectMeta{ Name: "manifest-1", @@ -473,7 +473,7 @@ func TestHandleListVulnerabilityManifests_FilterByNamespace(t *testing.T) { } func TestHandleListVulnerabilityManifests_EmptyResults(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleListVulnerabilityManifests(context.Background(), makeRequest(nil)) @@ -497,7 +497,7 @@ func TestHandleListVulnerabilityManifests_InitError(t *testing.T) { } func TestHandleListVulnerabilitiesInManifest_Success(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.VulnerabilityManifest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-manifest", @@ -549,7 +549,7 @@ func TestHandleListVulnerabilitiesInManifest_Success(t *testing.T) { } func TestHandleListVulnerabilitiesInManifest_MissingManifestName(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleListVulnerabilitiesInManifest(context.Background(), makeRequest(nil)) @@ -560,7 +560,7 @@ func TestHandleListVulnerabilitiesInManifest_MissingManifestName(t *testing.T) { } func TestHandleListVulnerabilitiesInManifest_ManifestNotFound(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleListVulnerabilitiesInManifest(context.Background(), makeRequest(map[string]interface{}{ @@ -572,7 +572,7 @@ func TestHandleListVulnerabilitiesInManifest_ManifestNotFound(t *testing.T) { } func TestHandleGetVulnerabilityDetails_Success(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.VulnerabilityManifest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-manifest", @@ -619,7 +619,7 @@ func TestHandleGetVulnerabilityDetails_Success(t *testing.T) { } func TestHandleGetVulnerabilityDetails_MissingManifestName(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleGetVulnerabilityDetails(context.Background(), makeRequest(map[string]interface{}{ @@ -632,7 +632,7 @@ func TestHandleGetVulnerabilityDetails_MissingManifestName(t *testing.T) { } func TestHandleGetVulnerabilityDetails_MissingCveId(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleGetVulnerabilityDetails(context.Background(), makeRequest(map[string]interface{}{ @@ -645,7 +645,7 @@ func TestHandleGetVulnerabilityDetails_MissingCveId(t *testing.T) { } func TestHandleGetVulnerabilityDetails_CveNotFound(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.VulnerabilityManifest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-manifest", @@ -672,7 +672,7 @@ func TestHandleGetVulnerabilityDetails_CveNotFound(t *testing.T) { } func TestHandleListConfigurationScans_Success(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.WorkloadConfigurationScan{ ObjectMeta: metav1.ObjectMeta{ Name: "scan-1", @@ -702,7 +702,7 @@ func TestHandleListConfigurationScans_Success(t *testing.T) { } func TestHandleListConfigurationScans_FilterByNamespace(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.WorkloadConfigurationScan{ ObjectMeta: metav1.ObjectMeta{ Name: "scan-1", @@ -727,7 +727,7 @@ func TestHandleListConfigurationScans_FilterByNamespace(t *testing.T) { } func TestHandleListConfigurationScans_EmptyResults(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleListConfigurationScans(context.Background(), makeRequest(nil)) @@ -742,7 +742,7 @@ func TestHandleListConfigurationScans_EmptyResults(t *testing.T) { } func TestHandleGetConfigurationScan_Success(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.WorkloadConfigurationScan{ ObjectMeta: metav1.ObjectMeta{ Name: "test-scan", @@ -762,7 +762,7 @@ func TestHandleGetConfigurationScan_Success(t *testing.T) { } func TestHandleGetConfigurationScan_MissingManifestName(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleGetConfigurationScan(context.Background(), makeRequest(nil)) @@ -773,7 +773,7 @@ func TestHandleGetConfigurationScan_MissingManifestName(t *testing.T) { } func TestHandleGetConfigurationScan_NotFound(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleGetConfigurationScan(context.Background(), makeRequest(map[string]interface{}{ @@ -806,7 +806,7 @@ func TestTruncateString(t *testing.T) { } func TestNilArgumentsHandling(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) // Test with nil arguments map - should use defaults @@ -822,7 +822,7 @@ func TestNilArgumentsHandling(t *testing.T) { // Tests for ApplicationProfile handlers func TestHandleListApplicationProfiles_Success(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.ApplicationProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "profile-1", @@ -869,7 +869,7 @@ func TestHandleListApplicationProfiles_Success(t *testing.T) { } func TestHandleListApplicationProfiles_FilterByNamespace(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.ApplicationProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "profile-1", @@ -894,7 +894,7 @@ func TestHandleListApplicationProfiles_FilterByNamespace(t *testing.T) { } func TestHandleListApplicationProfiles_EmptyResults(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleListApplicationProfiles(context.Background(), makeRequest(nil)) @@ -918,7 +918,7 @@ func TestHandleListApplicationProfiles_InitError(t *testing.T) { } func TestHandleGetApplicationProfile_Success(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.ApplicationProfile{ ObjectMeta: metav1.ObjectMeta{ Name: "test-profile", @@ -962,7 +962,7 @@ func TestHandleGetApplicationProfile_Success(t *testing.T) { } func TestHandleGetApplicationProfile_MissingName(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleGetApplicationProfile(context.Background(), makeRequest(map[string]interface{}{ @@ -975,7 +975,7 @@ func TestHandleGetApplicationProfile_MissingName(t *testing.T) { } func TestHandleGetApplicationProfile_MissingNamespace(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleGetApplicationProfile(context.Background(), makeRequest(map[string]interface{}{ @@ -988,7 +988,7 @@ func TestHandleGetApplicationProfile_MissingNamespace(t *testing.T) { } func TestHandleGetApplicationProfile_NotFound(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleGetApplicationProfile(context.Background(), makeRequest(map[string]interface{}{ @@ -1003,7 +1003,7 @@ func TestHandleGetApplicationProfile_NotFound(t *testing.T) { // Tests for NetworkNeighborhood handlers func TestHandleListNetworkNeighborhoods_Success(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.NetworkNeighborhood{ ObjectMeta: metav1.ObjectMeta{ Name: "nn-1", @@ -1049,7 +1049,7 @@ func TestHandleListNetworkNeighborhoods_Success(t *testing.T) { } func TestHandleListNetworkNeighborhoods_FilterByNamespace(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.NetworkNeighborhood{ ObjectMeta: metav1.ObjectMeta{ Name: "nn-1", @@ -1074,7 +1074,7 @@ func TestHandleListNetworkNeighborhoods_FilterByNamespace(t *testing.T) { } func TestHandleListNetworkNeighborhoods_EmptyResults(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleListNetworkNeighborhoods(context.Background(), makeRequest(nil)) @@ -1098,7 +1098,7 @@ func TestHandleListNetworkNeighborhoods_InitError(t *testing.T) { } func TestHandleGetNetworkNeighborhood_Success(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset( + spdxClient := kubescapefake.NewClientset( &v1beta1.NetworkNeighborhood{ ObjectMeta: metav1.ObjectMeta{ Name: "test-nn", @@ -1140,7 +1140,7 @@ func TestHandleGetNetworkNeighborhood_Success(t *testing.T) { } func TestHandleGetNetworkNeighborhood_MissingName(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleGetNetworkNeighborhood(context.Background(), makeRequest(map[string]interface{}{ @@ -1153,7 +1153,7 @@ func TestHandleGetNetworkNeighborhood_MissingName(t *testing.T) { } func TestHandleGetNetworkNeighborhood_MissingNamespace(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleGetNetworkNeighborhood(context.Background(), makeRequest(map[string]interface{}{ @@ -1166,7 +1166,7 @@ func TestHandleGetNetworkNeighborhood_MissingNamespace(t *testing.T) { } func TestHandleGetNetworkNeighborhood_NotFound(t *testing.T) { - spdxClient := kubescapefake.NewSimpleClientset() + spdxClient := kubescapefake.NewClientset() tool := NewKubescapeToolWithClients(nil, nil, spdxClient.SpdxV1beta1()) result, err := tool.HandleGetNetworkNeighborhood(context.Background(), makeRequest(map[string]interface{}{ From 9c01524de74863bd0a03790095d393883847116e Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Mon, 16 Feb 2026 21:14:37 -0500 Subject: [PATCH 31/41] feat: add token support for kubectl commands (#37) * feat: add token support for kubectl commands Signed-off-by: Eitan Yarmush * use pre-v4 helm version Signed-off-by: Eitan Yarmush * Add configuration to disable service token automount Signed-off-by: Jeremy Alvis * Remove automountServiceAccountToken config Signed-off-by: Jeremy Alvis * helm config for using default service account Signed-off-by: Jeremy Alvis * Add tools.k8s.tokenPassthrough for requiring token from auth header Signed-off-by: Jeremy Alvis * Fix helm version Signed-off-by: Jeremy Alvis * Remove automountServiceAccountToken from helm test Signed-off-by: Jeremy Alvis * Redact tokens Signed-off-by: Jeremy Alvis --------- Signed-off-by: Eitan Yarmush Signed-off-by: Jeremy Alvis Co-authored-by: Jeremy Alvis --- .github/workflows/ci.yaml | 2 +- helm/kagent-tools/templates/_helpers.tpl | 7 + helm/kagent-tools/templates/clusterrole.yaml | 4 +- .../templates/clusterrolebinding.yaml | 4 +- helm/kagent-tools/templates/deployment.yaml | 4 +- .../templates/serviceaccount.yaml | 4 +- helm/kagent-tools/tests/deployment_test.yaml | 35 +- helm/kagent-tools/values.yaml | 8 + internal/cmd/cmd.go | 7 +- internal/commands/builder.go | 32 +- internal/logger/logger.go | 24 +- internal/logger/logger_test.go | 37 ++ pkg/k8s/k8s.go | 116 +++-- pkg/k8s/k8s_test.go | 472 ++++++++++++++++++ 14 files changed, 695 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 993c2e55..dd105ca4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -87,7 +87,7 @@ jobs: - name: Set up Helm uses: azure/setup-helm@v4.2.0 with: - version: v4.1.0 + version: v4.1.1 - name: Install unittest plugin run: | diff --git a/helm/kagent-tools/templates/_helpers.tpl b/helm/kagent-tools/templates/_helpers.tpl index 7922df08..d47a01e2 100644 --- a/helm/kagent-tools/templates/_helpers.tpl +++ b/helm/kagent-tools/templates/_helpers.tpl @@ -65,6 +65,13 @@ Allows overriding it for multi-namespace deployments in combined charts. {{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} {{- end }} +{{/* +Service account name: default when useDefaultServiceAccount is true, otherwise the chart fullname. +*/}} +{{- define "kagent.serviceAccountName" -}} +{{- if .Values.useDefaultServiceAccount }}default{{- else }}{{ include "kagent.fullname" . }}{{- end }} +{{- end }} + {{/* Watch namespaces - transforms list of namespaces cached by the controller into comma-separated string Removes duplicates diff --git a/helm/kagent-tools/templates/clusterrole.yaml b/helm/kagent-tools/templates/clusterrole.yaml index cde9f45e..cbd50bde 100644 --- a/helm/kagent-tools/templates/clusterrole.yaml +++ b/helm/kagent-tools/templates/clusterrole.yaml @@ -1,3 +1,4 @@ +{{- if not .Values.useDefaultServiceAccount }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -26,4 +27,5 @@ rules: verbs: - get - list - - watch \ No newline at end of file + - watch +{{- end }} \ No newline at end of file diff --git a/helm/kagent-tools/templates/clusterrolebinding.yaml b/helm/kagent-tools/templates/clusterrolebinding.yaml index ee7d67e8..bbe51eb7 100644 --- a/helm/kagent-tools/templates/clusterrolebinding.yaml +++ b/helm/kagent-tools/templates/clusterrolebinding.yaml @@ -1,3 +1,4 @@ +{{- if not .Values.useDefaultServiceAccount }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: @@ -41,4 +42,5 @@ roleRef: subjects: - kind: ServiceAccount name: {{ include "kagent.fullname" . }} - namespace: {{ include "kagent.namespace" . }} \ No newline at end of file + namespace: {{ include "kagent.namespace" . }} +{{- end }} \ No newline at end of file diff --git a/helm/kagent-tools/templates/deployment.yaml b/helm/kagent-tools/templates/deployment.yaml index 001caef2..24d324ef 100644 --- a/helm/kagent-tools/templates/deployment.yaml +++ b/helm/kagent-tools/templates/deployment.yaml @@ -51,7 +51,7 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} - serviceAccountName: {{ include "kagent.fullname" . }} + serviceAccountName: {{ include "kagent.serviceAccountName" . }} containers: - name: tools command: @@ -91,6 +91,8 @@ spec: value: {{ .Values.otel.tracing.exporter.otlp.timeout | quote }} - name: OTEL_EXPORTER_OTLP_TRACES_INSECURE value: {{ .Values.otel.tracing.exporter.otlp.insecure | quote }} + - name: TOKEN_PASSTHROUGH + value: {{ (index .Values.tools "k8s" | default dict).tokenPassthrough | default false | quote }} {{- with .Values.tools.env }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/helm/kagent-tools/templates/serviceaccount.yaml b/helm/kagent-tools/templates/serviceaccount.yaml index b0b4c03d..3422acb5 100644 --- a/helm/kagent-tools/templates/serviceaccount.yaml +++ b/helm/kagent-tools/templates/serviceaccount.yaml @@ -1,7 +1,9 @@ +{{- if not .Values.useDefaultServiceAccount }} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "kagent.fullname" . }} namespace: {{ include "kagent.namespace" . }} labels: - {{- include "kagent.labels" . | nindent 4 }} \ No newline at end of file + {{- include "kagent.labels" . | nindent 4 }} +{{- end }} diff --git a/helm/kagent-tools/tests/deployment_test.yaml b/helm/kagent-tools/tests/deployment_test.yaml index 397fd41a..0e4e8cac 100644 --- a/helm/kagent-tools/tests/deployment_test.yaml +++ b/helm/kagent-tools/tests/deployment_test.yaml @@ -60,13 +60,46 @@ tests: path: spec.template.spec.containers[0].resources.limits.memory value: 512Mi - - it: should have correct service account name + - it: should use default service account when useDefaultServiceAccount is true template: deployment.yaml + set: + useDefaultServiceAccount: true + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: default + + - it: should use dedicated service account when useDefaultServiceAccount is false + template: deployment.yaml + set: + useDefaultServiceAccount: false asserts: - equal: path: spec.template.spec.serviceAccountName value: RELEASE-NAME + - it: should set token passthrough env when tools.k8s.tokenPassthrough is true + template: deployment.yaml + set: + tools.k8s.tokenPassthrough: true + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: TOKEN_PASSTHROUGH + value: "true" + + - it: should set token passthrough env when tools.k8s.tokenPassthrough is false + template: deployment.yaml + set: + tools.k8s.tokenPassthrough: false + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: TOKEN_PASSTHROUGH + value: "false" + - it: should have correct container port template: deployment.yaml asserts: diff --git a/helm/kagent-tools/values.yaml b/helm/kagent-tools/values.yaml index 556f56ef..dbe150ee 100644 --- a/helm/kagent-tools/values.yaml +++ b/helm/kagent-tools/values.yaml @@ -1,6 +1,10 @@ # Default values for kagent replicaCount: 1 +# When true: pods use the default service account and no ClusterRole/ClusterRoleBinding are created. +# When false: a dedicated ServiceAccount and RBAC are created. +useDefaultServiceAccount: false + global: tag: "" @@ -27,6 +31,10 @@ tools: limits: cpu: 1000m memory: 512Mi + k8s: + # When true: a Bearer token in the Authorization header on each request is passed to kubectl; fails if missing + # When false: kubectl uses in-cluster ServiceAccount. + tokenPassthrough: false prometheus: url: "prometheus.kagent.svc.cluster.local:9090" username: "" diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 30610061..0fa2741f 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -20,10 +20,11 @@ type DefaultShellExecutor struct{} func (e *DefaultShellExecutor) Exec(ctx context.Context, command string, args ...string) ([]byte, error) { log := logger.WithContext(ctx) startTime := time.Now() + redactedArgs := logger.RedactArgsForLog(args) log.Info("executing command", "command", command, - "args", args, + "args", redactedArgs, ) cmd := exec.CommandContext(ctx, command, args...) @@ -34,7 +35,7 @@ func (e *DefaultShellExecutor) Exec(ctx context.Context, command string, args .. if err != nil { log.Error("command execution failed", "command", command, - "args", args, + "args", redactedArgs, "error", err, "output", string(output), "duration", duration.Seconds(), @@ -42,7 +43,7 @@ func (e *DefaultShellExecutor) Exec(ctx context.Context, command string, args .. } else { log.Info("command execution successful", "command", command, - "args", args, + "args", redactedArgs, "duration", duration.Seconds(), ) } diff --git a/internal/commands/builder.go b/internal/commands/builder.go index 3bced945..c9baed83 100644 --- a/internal/commands/builder.go +++ b/internal/commands/builder.go @@ -29,6 +29,7 @@ type CommandBuilder struct { namespace string context string kubeconfig string + token string output string labels map[string]string annotations map[string]string @@ -120,6 +121,14 @@ func (cb *CommandBuilder) WithKubeconfig(kubeconfig string) *CommandBuilder { return cb } +// WithToken sets the authentication token for kubectl commands +func (cb *CommandBuilder) WithToken(token string) *CommandBuilder { + if token != "" { + cb.token = token + } + return cb +} + // WithOutput sets the output format func (cb *CommandBuilder) WithOutput(output string) *CommandBuilder { validOutputs := []string{"json", "yaml", "wide", "name", "custom-columns", "custom-columns-file", "go-template", "go-template-file", "jsonpath", "jsonpath-file"} @@ -240,6 +249,11 @@ func (cb *CommandBuilder) Build() (string, []string, error) { args = append(args, "--kubeconfig", cb.kubeconfig) } + // Add token if specified + if cb.token != "" { + args = append(args, "--token", cb.token) + } + // Add output format if cb.output != "" { args = append(args, "--output", cb.output) @@ -293,7 +307,7 @@ func (cb *CommandBuilder) Execute(ctx context.Context) (string, error) { log := logger.WithContext(ctx) _, span := telemetry.StartSpan(ctx, "commands.execute", attribute.String("command", cb.command), - attribute.StringSlice("args", cb.args), + attribute.StringSlice("args", logger.RedactArgsForLog(cb.args)), attribute.Bool("cached", cb.cached), ) defer span.End() @@ -308,14 +322,15 @@ func (cb *CommandBuilder) Execute(ctx context.Context) (string, error) { return "", err } + redactedArgs := logger.RedactArgsForLog(args) span.SetAttributes( attribute.String("built_command", command), - attribute.StringSlice("built_args", args), + attribute.StringSlice("built_args", redactedArgs), ) log.Debug("executing command", "command", command, - "args", args, + "args", redactedArgs, "cached", cb.cached, ) @@ -343,9 +358,10 @@ func (cb *CommandBuilder) Execute(ctx context.Context) (string, error) { func (cb *CommandBuilder) executeWithCache(ctx context.Context, command string, args []string) (string, error) { log := logger.WithContext(ctx) + redactedArgs := logger.RedactArgsForLog(args) _, span := telemetry.StartSpan(ctx, "commands.executeWithCache", attribute.String("command", command), - attribute.StringSlice("args", args), + attribute.StringSlice("args", redactedArgs), attribute.Bool("cached", true), ) defer span.End() @@ -357,7 +373,7 @@ func (cb *CommandBuilder) executeWithCache(ctx context.Context, command string, log.Info("executing cached command", "command", command, - "args", args, + "args", redactedArgs, "cache_key", cacheKey, "cache_ttl", cb.cacheTTL.String(), ) @@ -374,7 +390,7 @@ func (cb *CommandBuilder) executeWithCache(ctx context.Context, command string, telemetry.AddEvent(span, "cache.miss.executing_command") log.Debug("cache miss, executing command", "command", command, - "args", args, + "args", redactedArgs, ) return cb.executeCommand(ctx, command, args) }) @@ -383,7 +399,7 @@ func (cb *CommandBuilder) executeWithCache(ctx context.Context, command string, telemetry.RecordError(span, err, "Cached command execution failed") log.Error("cached command execution failed", "command", command, - "args", args, + "args", redactedArgs, "cache_key", cacheKey, "error", err, ) @@ -393,7 +409,7 @@ func (cb *CommandBuilder) executeWithCache(ctx context.Context, command string, telemetry.RecordSuccess(span, "Cached command executed successfully") log.Info("cached command execution successful", "command", command, - "args", args, + "args", redactedArgs, "cache_key", cacheKey, "result_length", len(result), ) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index b9a078f2..0569689d 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -59,19 +59,37 @@ func WithContext(ctx context.Context) *slog.Logger { return logger } +// RedactArgsForLog returns a copy of args with sensitive values redacted for logging. +// Any value immediately following "--token" is replaced with "" so tokens are not logged. +func RedactArgsForLog(args []string) []string { + if len(args) == 0 { + return nil + } + out := make([]string, len(args)) + copy(out, args) + for i := 0; i < len(out)-1; i++ { + if out[i] == "--token" { + out[i+1] = "" + i++ // skip the redacted value + } + } + return out +} + func LogExecCommand(ctx context.Context, logger *slog.Logger, command string, args []string, caller string) { logger.Info("executing command", "command", command, - "args", args, + "args", RedactArgsForLog(args), "caller", caller, ) } func LogExecCommandResult(ctx context.Context, logger *slog.Logger, command string, args []string, output string, err error, duration float64, caller string) { + redacted := RedactArgsForLog(args) if err != nil { logger.Error("command execution failed", "command", command, - "args", args, + "args", redacted, "error", err.Error(), "duration_seconds", duration, "caller", caller, @@ -79,7 +97,7 @@ func LogExecCommandResult(ctx context.Context, logger *slog.Logger, command stri } else { logger.Info("command execution successful", "command", command, - "args", args, + "args", redacted, "output", output, "duration_seconds", duration, "caller", caller, diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index efca71e7..f6befc5c 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -12,6 +12,43 @@ import ( "go.opentelemetry.io/otel/trace/noop" ) +func TestRedactArgsForLog(t *testing.T) { + t.Run("redacts token value", func(t *testing.T) { + args := []string{"get", "pods", "--token", "secret-token-123", "-n", "default"} + redacted := RedactArgsForLog(args) + require.Len(t, redacted, 6) + assert.Equal(t, "get", redacted[0]) + assert.Equal(t, "pods", redacted[1]) + assert.Equal(t, "--token", redacted[2]) + assert.Equal(t, "", redacted[3]) + assert.Equal(t, "-n", redacted[4]) + assert.Equal(t, "default", redacted[5]) + }) + t.Run("empty args returns nil", func(t *testing.T) { + assert.Nil(t, RedactArgsForLog(nil)) + assert.Nil(t, RedactArgsForLog([]string{})) + }) + t.Run("args without token unchanged", func(t *testing.T) { + args := []string{"get", "pods", "-n", "default"} + redacted := RedactArgsForLog(args) + assert.Equal(t, args, redacted) + }) + t.Run("--token at end with no value", func(t *testing.T) { + args := []string{"get", "pods", "--token"} + redacted := RedactArgsForLog(args) + assert.Equal(t, args, redacted) + }) + t.Run("logged output does not contain token", func(t *testing.T) { + var buf bytes.Buffer + log := slog.New(slog.NewTextHandler(&buf, nil)) + args := []string{"get", "pods", "--token", "secret-token-123"} + log.Info("executing command", "command", "kubectl", "args", RedactArgsForLog(args)) + output := buf.String() + assert.Contains(t, output, "") + assert.NotContains(t, output, "secret-token-123") + }) +} + func TestLogExecCommand(t *testing.T) { var buf bytes.Buffer logger := slog.New(slog.NewTextHandler(&buf, nil)) diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index f9184d12..22a8badf 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -6,6 +6,7 @@ import ( "fmt" "maps" "math/rand" + "net/http" "os" "slices" "strings" @@ -24,21 +25,22 @@ import ( // K8sTool struct to hold the LLM model type K8sTool struct { - kubeconfig string - llmModel llms.Model + kubeconfig string + llmModel llms.Model + tokenPassthrough bool // when true, require Bearer token and pass it to kubectl; when false, do not use token } func NewK8sTool(llmModel llms.Model) *K8sTool { - return &K8sTool{llmModel: llmModel} + return &K8sTool{llmModel: llmModel, tokenPassthrough: os.Getenv("TOKEN_PASSTHROUGH") == "true"} } func NewK8sToolWithConfig(kubeconfig string, llmModel llms.Model) *K8sTool { - return &K8sTool{kubeconfig: kubeconfig, llmModel: llmModel} + return &K8sTool{kubeconfig: kubeconfig, llmModel: llmModel, tokenPassthrough: os.Getenv("TOKEN_PASSTHROUGH") == "true"} } // runKubectlCommandWithCacheInvalidation runs a kubectl command and invalidates cache if it's a modification operation -func (k *K8sTool) runKubectlCommandWithCacheInvalidation(ctx context.Context, args ...string) (*mcp.CallToolResult, error) { - result, err := k.runKubectlCommand(ctx, args...) +func (k *K8sTool) runKubectlCommandWithCacheInvalidation(ctx context.Context, headers http.Header, args ...string) (*mcp.CallToolResult, error) { + result, err := k.runKubectlCommand(ctx, headers, args...) // If command succeeded and it's a modification command, invalidate cache if err == nil && len(args) > 0 { @@ -82,7 +84,7 @@ func (k *K8sTool) handleKubectlGetEnhanced(ctx context.Context, request mcp.Call args = append(args, "-o", "json") } - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Get pod logs @@ -106,7 +108,7 @@ func (k *K8sTool) handleKubectlLogsEnhanced(ctx context.Context, request mcp.Cal args = append(args, "--tail", fmt.Sprintf("%d", tailLines)) } - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Scale deployment @@ -121,7 +123,7 @@ func (k *K8sTool) handleScaleDeployment(ctx context.Context, request mcp.CallToo args := []string{"scale", "deployment", deploymentName, "--replicas", fmt.Sprintf("%d", replicas), "-n", namespace} - return k.runKubectlCommandWithCacheInvalidation(ctx, args...) + return k.runKubectlCommandWithCacheInvalidation(ctx, request.Header, args...) } // Patch resource @@ -152,7 +154,7 @@ func (k *K8sTool) handlePatchResource(ctx context.Context, request mcp.CallToolR args := []string{"patch", resourceType, resourceName, "-p", patch, "-n", namespace} - return k.runKubectlCommandWithCacheInvalidation(ctx, args...) + return k.runKubectlCommandWithCacheInvalidation(ctx, request.Header, args...) } // Apply manifest from content @@ -197,7 +199,7 @@ func (k *K8sTool) handleApplyManifest(ctx context.Context, request mcp.CallToolR return mcp.NewToolResultError(fmt.Sprintf("Failed to close temp file: %v", err)), nil } - return k.runKubectlCommandWithCacheInvalidation(ctx, "apply", "-f", tmpFile.Name()) + return k.runKubectlCommandWithCacheInvalidation(ctx, request.Header, "apply", "-f", tmpFile.Name()) } // Delete resource @@ -212,7 +214,7 @@ func (k *K8sTool) handleDeleteResource(ctx context.Context, request mcp.CallTool args := []string{"delete", resourceType, resourceName, "-n", namespace} - return k.runKubectlCommandWithCacheInvalidation(ctx, args...) + return k.runKubectlCommandWithCacheInvalidation(ctx, request.Header, args...) } // Check service connectivity @@ -227,23 +229,23 @@ func (k *K8sTool) handleCheckServiceConnectivity(ctx context.Context, request mc // Create a temporary curl pod for connectivity check podName := fmt.Sprintf("curl-test-%d", rand.Intn(10000)) defer func() { - _, _ = k.runKubectlCommand(ctx, "delete", "pod", podName, "-n", namespace, "--ignore-not-found") + _, _ = k.runKubectlCommand(ctx, request.Header, "delete", "pod", podName, "-n", namespace, "--ignore-not-found") }() // Create the curl pod - _, err := k.runKubectlCommand(ctx, "run", podName, "--image=curlimages/curl", "-n", namespace, "--restart=Never", "--", "sleep", "3600") + _, err := k.runKubectlCommand(ctx, request.Header, "run", podName, "--image=curlimages/curl", "-n", namespace, "--restart=Never", "--", "sleep", "3600") if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to create curl pod: %v", err)), nil } // Wait for pod to be ready - _, err = k.runKubectlCommandWithTimeout(ctx, 60*time.Second, "wait", "--for=condition=ready", "pod/"+podName, "-n", namespace) + _, err = k.runKubectlCommandWithTimeout(ctx, request.Header, 60*time.Second, "wait", "--for=condition=ready", "pod/"+podName, "-n", namespace) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to wait for curl pod: %v", err)), nil } // Execute kubectl command - return k.runKubectlCommand(ctx, "exec", podName, "-n", namespace, "--", "curl", "-s", serviceName) + return k.runKubectlCommand(ctx, request.Header, "exec", podName, "-n", namespace, "--", "curl", "-s", serviceName) } // Get cluster events @@ -257,7 +259,7 @@ func (k *K8sTool) handleGetEvents(ctx context.Context, request mcp.CallToolReque args = append(args, "--all-namespaces") } - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Execute command in pod @@ -287,12 +289,12 @@ func (k *K8sTool) handleExecCommand(ctx context.Context, request mcp.CallToolReq args := []string{"exec", podName, "-n", namespace, "--", command} - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Get available API resources func (k *K8sTool) handleGetAvailableAPIResources(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return k.runKubectlCommand(ctx, "api-resources") + return k.runKubectlCommand(ctx, request.Header, "api-resources") } // Kubectl describe tool @@ -310,7 +312,7 @@ func (k *K8sTool) handleKubectlDescribeTool(ctx context.Context, request mcp.Cal args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Rollout operations @@ -329,12 +331,12 @@ func (k *K8sTool) handleRollout(ctx context.Context, request mcp.CallToolRequest args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Get cluster configuration func (k *K8sTool) handleGetClusterConfiguration(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return k.runKubectlCommand(ctx, "config", "view", "-o", "json") + return k.runKubectlCommand(ctx, request.Header, "config", "view", "-o", "json") } // Remove annotation @@ -353,7 +355,7 @@ func (k *K8sTool) handleRemoveAnnotation(ctx context.Context, request mcp.CallTo args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Remove label @@ -372,7 +374,7 @@ func (k *K8sTool) handleRemoveLabel(ctx context.Context, request mcp.CallToolReq args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Annotate resource @@ -393,7 +395,7 @@ func (k *K8sTool) handleAnnotateResource(ctx context.Context, request mcp.CallTo args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Label resource @@ -414,7 +416,7 @@ func (k *K8sTool) handleLabelResource(ctx context.Context, request mcp.CallToolR args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Create resource from URL @@ -431,7 +433,7 @@ func (k *K8sTool) handleCreateResourceFromURL(ctx context.Context, request mcp.C args = append(args, "-n", namespace) } - return k.runKubectlCommand(ctx, args...) + return k.runKubectlCommand(ctx, request.Header, args...) } // Resource generation embeddings @@ -528,32 +530,64 @@ func (k *K8sTool) handleGenerateResource(ctx context.Context, request mcp.CallTo return mcp.NewToolResultText(responseText), nil } +// extractBearerToken extracts the Bearer token from the Authorization header +func extractBearerToken(headers http.Header) string { + if auth := headers.Get("Authorization"); auth != "" { + if strings.HasPrefix(auth, "Bearer ") { + return strings.TrimPrefix(auth, "Bearer ") + } + } + return "" +} + +// tokenForKubectl returns the token to pass to kubectl and an error if passthrough is true but token is missing. +func (k *K8sTool) tokenForKubectl(headers http.Header) (string, error) { + token := extractBearerToken(headers) + if k.tokenPassthrough && token == "" { + return "", fmt.Errorf("Bearer token required when TOKEN_PASSTHROUGH is true") + } + if k.tokenPassthrough { + return token, nil + } + return "", nil // do not use token when passthrough is false +} + // runKubectlCommand is a helper function to execute kubectl commands -func (k *K8sTool) runKubectlCommand(ctx context.Context, args ...string) (*mcp.CallToolResult, error) { - output, err := commands.NewCommandBuilder("kubectl"). +func (k *K8sTool) runKubectlCommand(ctx context.Context, headers http.Header, args ...string) (*mcp.CallToolResult, error) { + token, err := k.tokenForKubectl(headers) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + builder := commands.NewCommandBuilder("kubectl"). WithArgs(args...). - WithKubeconfig(k.kubeconfig). - Execute(ctx) - + WithKubeconfig(k.kubeconfig) + if token != "" { + builder = builder.WithToken(token) + } + output, err := builder.Execute(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } - return mcp.NewToolResultText(output), nil } // runKubectlCommandWithTimeout is a helper function to execute kubectl commands with a timeout -func (k *K8sTool) runKubectlCommandWithTimeout(ctx context.Context, timeout time.Duration, args ...string) (*mcp.CallToolResult, error) { - output, err := commands.NewCommandBuilder("kubectl"). +func (k *K8sTool) runKubectlCommandWithTimeout(ctx context.Context, headers http.Header, timeout time.Duration, args ...string) (*mcp.CallToolResult, error) { + token, err := k.tokenForKubectl(headers) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + builder := commands.NewCommandBuilder("kubectl"). WithArgs(args...). WithKubeconfig(k.kubeconfig). - WithTimeout(timeout). - Execute(ctx) - + WithTimeout(timeout) + if token != "" { + builder = builder.WithToken(token) + } + output, err := builder.Execute(ctx) if err != nil { return mcp.NewToolResultError(err.Error()), nil } - return mcp.NewToolResultText(output), nil } @@ -611,7 +645,7 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string, readO args = append(args, "-n", namespace) } - result, err := k8sTool.runKubectlCommand(ctx, args...) + result, err := k8sTool.runKubectlCommand(ctx, request.Header, args...) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Get YAML command failed: %v", err)), nil } @@ -737,7 +771,7 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string, readO } tmpFile.Close() - result, err := k8sTool.runKubectlCommand(ctx, "create", "-f", tmpFile.Name()) + result, err := k8sTool.runKubectlCommand(ctx, request.Header, "create", "-f", tmpFile.Name()) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Create command failed: %v", err)), nil } diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go index e3730663..8ac03409 100644 --- a/pkg/k8s/k8s_test.go +++ b/pkg/k8s/k8s_test.go @@ -2,6 +2,7 @@ package k8s import ( "context" + "net/http" "testing" "github.com/kagent-dev/tools/internal/cmd" @@ -16,6 +17,13 @@ func newTestK8sTool() *K8sTool { return NewK8sTool(nil) } +// newTestK8sToolWithPassthrough creates a K8sTool with token passthrough set for testing. +func newTestK8sToolWithPassthrough(passthrough bool) *K8sTool { + t := NewK8sTool(nil) + t.tokenPassthrough = passthrough + return t +} + // Helper function to create a test K8sTool with mock LLM func newTestK8sToolWithLLM(llm llms.Model) *K8sTool { return NewK8sTool(llm) @@ -32,6 +40,21 @@ func getResultText(result *mcp.CallToolResult) string { return "" } +// Helper function to create an http.Header with Bearer token authorization +func headerWithBearerToken(token string) http.Header { + h := http.Header{} + h.Set("Authorization", "Bearer "+token) + return h +} + +// Helper function to create a CallToolRequest with Bearer token +func requestWithBearerToken(token string, args map[string]interface{}) mcp.CallToolRequest { + req := mcp.CallToolRequest{} + req.Header = headerWithBearerToken(token) + req.Params.Arguments = args + return req +} + func TestHandleGetAvailableAPIResources(t *testing.T) { ctx := context.Background() @@ -1063,3 +1086,452 @@ users: assert.Contains(t, resultText, "clusters") }) } + +// Tests for Bearer token passing to kubectl commands +func TestBearerTokenPassthrough(t *testing.T) { + ctx := context.Background() + + t.Run("get resources with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `NAME READY STATUS RESTARTS AGE` + mock.AddCommandString("kubectl", []string{"get", "pods", "-o", "wide", "--token", "test-token-123"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("test-token-123", map[string]interface{}{"resource_type": "pods"}) + result, err := k8sTool.handleKubectlGetEnhanced(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Verify the command was executed with the token + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Equal(t, "kubectl", callLog[0].Command) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "test-token-123") + }) + + t.Run("scale deployment with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment scaled` + mock.AddCommandString("kubectl", []string{"scale", "deployment", "test-deployment", "--replicas", "5", "-n", "default", "--token", "my-auth-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("my-auth-token", map[string]interface{}{ + "name": "test-deployment", + "replicas": float64(5), + }) + + result, err := k8sTool.handleScaleDeployment(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Verify the command was executed with the token + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "my-auth-token") + }) + + t.Run("get pod logs with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `log line 1 +log line 2` + mock.AddCommandString("kubectl", []string{"logs", "test-pod", "-n", "default", "--tail", "50", "--token", "logs-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("logs-token", map[string]interface{}{"pod_name": "test-pod"}) + result, err := k8sTool.handleKubectlLogsEnhanced(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "logs-token") + }) + + t.Run("delete resource with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment deleted` + mock.AddCommandString("kubectl", []string{"delete", "deployment", "test-deployment", "-n", "default", "--token", "delete-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("delete-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + }) + + result, err := k8sTool.handleDeleteResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "delete-token") + }) + + t.Run("patch resource with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment patched` + mock.AddCommandString("kubectl", []string{"patch", "deployment", "test-deployment", "-p", `{"spec":{"replicas":5}}`, "-n", "default", "--token", "patch-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("patch-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "patch": `{"spec":{"replicas":5}}`, + }) + + result, err := k8sTool.handlePatchResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "patch-token") + }) + + t.Run("describe resource with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `Name: test-deployment` + mock.AddCommandString("kubectl", []string{"describe", "deployment", "test-deployment", "-n", "default", "--token", "describe-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("describe-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "namespace": "default", + }) + + result, err := k8sTool.handleKubectlDescribeTool(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "describe-token") + }) + + t.Run("rollout with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/myapp restarted` + mock.AddCommandString("kubectl", []string{"rollout", "restart", "deployment/myapp", "-n", "default", "--token", "rollout-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("rollout-token", map[string]interface{}{ + "action": "restart", + "resource_type": "deployment", + "resource_name": "myapp", + "namespace": "default", + }) + + result, err := k8sTool.handleRollout(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "rollout-token") + }) + + t.Run("get events with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `{"items": []}` + mock.AddCommandString("kubectl", []string{"get", "events", "-o", "json", "--all-namespaces", "--token", "events-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("events-token", nil) + result, err := k8sTool.handleGetEvents(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "events-token") + }) + + t.Run("exec command with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `total 8` + mock.AddCommandString("kubectl", []string{"exec", "mypod", "-n", "default", "--", "ls -la", "--token", "exec-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("exec-token", map[string]interface{}{ + "pod_name": "mypod", + "namespace": "default", + "command": "ls -la", + }) + + result, err := k8sTool.handleExecCommand(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "exec-token") + }) + + t.Run("annotate resource with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment annotated` + mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1=value1", "--token", "annotate-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("annotate-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "annotations": "key1=value1", + }) + + result, err := k8sTool.handleAnnotateResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "annotate-token") + }) + + t.Run("label resource with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment labeled` + mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env=prod", "--token", "label-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("label-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "labels": "env=prod", + }) + + result, err := k8sTool.handleLabelResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "label-token") + }) + + t.Run("api resources with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `NAME SHORTNAMES APIVERSION NAMESPACED KIND` + mock.AddCommandString("kubectl", []string{"api-resources", "--token", "api-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("api-token", nil) + result, err := k8sTool.handleGetAvailableAPIResources(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "api-token") + }) + + t.Run("cluster configuration with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `{"current-context": "default"}` + mock.AddCommandString("kubectl", []string{"config", "view", "-o", "json", "--token", "config-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("config-token", nil) + result, err := k8sTool.handleGetClusterConfiguration(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "config-token") + }) + + t.Run("remove annotation with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment annotated` + mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1-", "--token", "remove-anno-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("remove-anno-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "annotation_key": "key1", + }) + + result, err := k8sTool.handleRemoveAnnotation(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "remove-anno-token") + }) + + t.Run("remove label with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment labeled` + mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env-", "--token", "remove-label-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("remove-label-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "label_key": "env", + }) + + result, err := k8sTool.handleRemoveLabel(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "remove-label-token") + }) + + t.Run("create resource from URL with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment created` + mock.AddCommandString("kubectl", []string{"create", "-f", "https://example.com/manifest.yaml", "-n", "default", "--token", "url-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("url-token", map[string]interface{}{ + "url": "https://example.com/manifest.yaml", + "namespace": "default", + }) + + result, err := k8sTool.handleCreateResourceFromURL(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "url-token") + }) + + t.Run("apply manifest with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + manifest := `apiVersion: v1 +kind: Pod +metadata: + name: test-pod` + expectedOutput := `pod/test-pod created` + // Use partial matcher since temp file name is dynamic + mock.AddPartialMatcherString("kubectl", []string{"apply", "-f"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(true) + req := requestWithBearerToken("apply-token", map[string]interface{}{ + "manifest": manifest, + }) + + result, err := k8sTool.handleApplyManifest(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "apply-token") + }) + + t.Run("returns error when passthrough true and authorization header missing", func(t *testing.T) { + k8sTool := newTestK8sToolWithPassthrough(true) + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{"resource_type": "pods"} + result, err := k8sTool.handleKubectlGetEnhanced(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, getResultText(result), "Bearer token required") + }) + + t.Run("no token when passthrough false and authorization header missing", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `NAME READY STATUS RESTARTS AGE` + // No --token in expected args when passthrough is false + mock.AddCommandString("kubectl", []string{"get", "pods", "-o", "wide"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(false) + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{"resource_type": "pods"} + // No Header set on request + result, err := k8sTool.handleKubectlGetEnhanced(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Verify no --token was added + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.NotContains(t, callLog[0].Args, "--token") + }) + + t.Run("no token when passthrough false and authorization header is not bearer", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `NAME READY STATUS RESTARTS AGE` + // No --token when passthrough is false + mock.AddCommandString("kubectl", []string{"get", "pods", "-o", "wide"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sToolWithPassthrough(false) + req := mcp.CallToolRequest{} + req.Header = http.Header{} + req.Header.Set("Authorization", "Basic dXNlcjpwYXNz") + req.Params.Arguments = map[string]interface{}{"resource_type": "pods"} + result, err := k8sTool.handleKubectlGetEnhanced(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Verify no --token was added + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.NotContains(t, callLog[0].Args, "--token") + }) +} From fd7e46c1a7e2704d72b540371aa72eff62d1a25e Mon Sep 17 00:00:00 2001 From: Matteo Mori <169821341+MatteoMori8@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:48:25 +0000 Subject: [PATCH 32/41] feat(helm): configurable RBAC with read-only ClusterRole support (#46) Signed-off-by: Matteo Mori --- helm/kagent-tools/templates/clusterrole.yaml | 100 ++++++++++++++---- .../templates/clusterrolebinding.yaml | 42 ++------ helm/kagent-tools/values.yaml | 19 ++++ 3 files changed, 111 insertions(+), 50 deletions(-) diff --git a/helm/kagent-tools/templates/clusterrole.yaml b/helm/kagent-tools/templates/clusterrole.yaml index cbd50bde..bb6e654f 100644 --- a/helm/kagent-tools/templates/clusterrole.yaml +++ b/helm/kagent-tools/templates/clusterrole.yaml @@ -1,31 +1,95 @@ -{{- if not .Values.useDefaultServiceAccount }} +{{- if and (not .Values.useDefaultServiceAccount) .Values.rbac.create }} +{{- if .Values.rbac.readOnly }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ include "kagent.fullname" . }}-cluster-admin-role + name: {{ include "kagent.fullname" . }}-read-role labels: {{- include "kagent.labels" . | nindent 4 }} rules: -- apiGroups: ["*"] - resources: ["*"] - verbs: ["*"] -- nonResourceURLs: ["*"] - verbs: ["*"] ---- + # Core workload resources + - apiGroups: [""] + resources: + - pods + - services + - endpoints + - configmaps + - serviceaccounts + - persistentvolumeclaims + - replicationcontrollers + - namespaces + verbs: ["get", "list", "watch"] + + # Pod logs (subresource) + - apiGroups: [""] + resources: + - pods/log + verbs: ["get", "list"] + + # Events + - apiGroups: [""] + resources: + - events + verbs: ["get", "list", "watch"] + - apiGroups: ["events.k8s.io"] + resources: + - events + verbs: ["get", "list", "watch"] + + # Apps workloads + - apiGroups: ["apps"] + resources: + - deployments + - statefulsets + - daemonsets + - replicasets + verbs: ["get", "list", "watch"] + + # Batch workloads + - apiGroups: ["batch"] + resources: + - jobs + - cronjobs + verbs: ["get", "list", "watch"] + # Networking + - apiGroups: ["networking.k8s.io"] + resources: + - ingresses + - networkpolicies + verbs: ["get", "list", "watch"] + + # Autoscaling + - apiGroups: ["autoscaling"] + resources: + - horizontalpodautoscalers + verbs: ["get", "list", "watch"] + + {{- if .Values.rbac.allowSecrets }} + # Secrets (opt-in) + - apiGroups: [""] + resources: + - secrets + verbs: ["get", "list", "watch"] + {{- end }} + + {{- with .Values.rbac.additionalRules }} + # Additional user-defined rules + {{- toYaml . | nindent 2 }} + {{- end }} + +{{- else }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ include "kagent.fullname" . }}-read-role + name: {{ include "kagent.fullname" . }}-cluster-admin-role labels: {{- include "kagent.labels" . | nindent 4 }} rules: - - apiGroups: ["*"] - resources: ["*"] - verbs: ["*"] - - nonResourceURLs: ["*"] - verbs: - - get - - list - - watch -{{- end }} \ No newline at end of file +- apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] +- nonResourceURLs: ["*"] + verbs: ["*"] +{{- end }} +{{- end }} diff --git a/helm/kagent-tools/templates/clusterrolebinding.yaml b/helm/kagent-tools/templates/clusterrolebinding.yaml index bbe51eb7..b8503bec 100644 --- a/helm/kagent-tools/templates/clusterrolebinding.yaml +++ b/helm/kagent-tools/templates/clusterrolebinding.yaml @@ -1,46 +1,24 @@ -{{- if not .Values.useDefaultServiceAccount }} +{{- if and (not .Values.useDefaultServiceAccount) .Values.rbac.create }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: + {{- if .Values.rbac.readOnly }} + name: {{ include "kagent.fullname" . }}-read-rolebinding + {{- else }} name: {{ include "kagent.fullname" . }}-cluster-admin-rolebinding + {{- end }} labels: {{- include "kagent.labels" . | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole + {{- if .Values.rbac.readOnly }} + name: {{ include "kagent.fullname" . }}-read-role + {{- else }} name: {{ include "kagent.fullname" . }}-cluster-admin-role + {{- end }} subjects: - kind: ServiceAccount name: {{ include "kagent.fullname" . }} namespace: {{ include "kagent.namespace" . }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ include "kagent.fullname" . }}-getter-rolebinding - labels: - {{- include "kagent.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ include "kagent.fullname" . }}-getter-role -subjects: -- kind: ServiceAccount - name: {{ include "kagent.fullname" . }} - namespace: {{ include "kagent.namespace" . }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: {{ include "kagent.fullname" . }}-writer-rolebinding - labels: - {{- include "kagent.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: {{ include "kagent.fullname" . }}-writer-role -subjects: -- kind: ServiceAccount - name: {{ include "kagent.fullname" . }} - namespace: {{ include "kagent.namespace" . }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/helm/kagent-tools/values.yaml b/helm/kagent-tools/values.yaml index dbe150ee..9c683664 100644 --- a/helm/kagent-tools/values.yaml +++ b/helm/kagent-tools/values.yaml @@ -91,6 +91,25 @@ topologySpreadConstraints: [] # topologyKey: topology.kubernetes.io/zone # whenUnsatisfiable: DoNotSchedule +rbac: + # When false, no ClusterRole or ClusterRoleBinding are created. + # The ServiceAccount is still created allowing you to attach your own roles externally. + create: true + # When true, deploys a read-only ClusterRole (get, list, watch) instead of cluster-admin. + # Pairs well with the --read-only CLI flag which disables write operations at the application layer. + # Only applies when rbac.create is true. + # Defaults to False as to not introduce breaking changes. Worth revisiting later on. + readOnly: false + # When readOnly is true, grants read access to Secrets. Default: no access to Secrets. + # Ignored when readOnly is false (admin already has full access). + allowSecrets: false + # Extra rules appended to the read-only ClusterRole. Useful for granting read access to CRDs (Istio, Cilium, Argo, etc.). + # Ignored when readOnly is false (admin already has full access). + additionalRules: [] + # - apiGroups: ["networking.istio.io"] + # resources: ["virtualservices", "destinationrules", "gateways"] + # verbs: ["get", "list", "watch"] + otel: tracing: enabled: false From b4e356a8cc81aab68540bb3c79dbc8654fcbb8ae Mon Sep 17 00:00:00 2001 From: Matteo Mori Date: Fri, 27 Feb 2026 18:52:54 +0000 Subject: [PATCH 33/41] feat(metrics): implement Prometheus observability (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(metrics): implement Prometheus observability with dedicated server Replace generateRuntimeMetrics() with prometheus/client_golang and add flexible metrics server architecture supporting same-port or dedicated port deployment. Changes: - Add internal/metrics package with custom Prometheus registry - Configurable metrics port via --metrics-port flag (default: 8084) - Two-server architecture with proper WaitGroup coordination - Graceful shutdown for both main and metrics servers - Export kagent_tools_mcp_server_info (version metadata) - Export kagent_tools_mcp_registered_tools (tool providers) - Include Go runtime metrics (goroutines, memory, GC stats) - Include process metrics (CPU, memory, file descriptors) Architecture improvement: Move http.Server instantiation outside goroutines to prevent race condition between assignment and shutdown. Test coverage: 5 unit tests validating registry, collectors, and metrics.Signed-off-by: MatteoMori * feat(metrics): auto-register tool metrics using ListTools() diff Use MCPServer.ListTools() to automatically detect which tools each provider registers, eliminating the need to modify individual tool packages. The approach snapshots the tool list before and after each provider's RegisterTools() call, then records the newly added tools in Prometheus with the correct tool_provider label. This means: - Zero changes required in any pkg/ file - Future tools are automatically tracked - No risk of forgetting to add a metric for a new toolSigned-off-by: MatteoMori * feat(metrics): instrument tool handlers with invocation counters Add kagent_tools_mcp_invocations_total and kagent_tools_mcp_invocations_failure_total counters using the wrapper/middleware pattern. All handlers are centrally instrumented in wrapToolHandlersWithMetrics with zero changes to pkg/ files. Update README with Observability section and CLI flags reference.Signed-off-by: MatteoMori * feat(observability): add Helm chart support and Grafana dashboard Add comprehensive Prometheus Operator integration via Helm chart: - ServiceMonitor resource for automatic target discovery - Dedicated metrics service (kagent-tools-metrics) - Deployment args for --metrics-port configuration - Configurable scrape interval, timeout, and labels Include Grafana dashboard with 8 panels visualizing: - Server version and health metrics - Tool invocation rates by provider - Success/failure rates and trends - Top invoked tools table with heat mapping Add CLAUDE.md with architecture documentation covering: - Tool provider pattern and MCP server lifecycle - Observability architecture (metrics wrapper pattern) - Development commands and key implementation patterns - Helm chart structure and troubleshooting guideSigned-off-by: MatteoMori * fix(metrics): default metrics-port to 0 (same as --port) Previously --metrics-port defaulted to 8084, causing a mismatch when the server ran on any other port (e.g. E2E tests use port 18190). The metrics server would start on 8084 instead of sharing the main port, so /metrics was unreachable at the expected address. Change the default to 0, resolved at runtime as "same as --port". Update Helm templates to fall back to the main targetPort when tools.metrics.port is unset. Signed-off-by: MatteoMori * fix(metrics): count result.IsError as invocation failure The failure counter previously only incremented on non-nil Go errors. Handlers in this codebase signal tool-level failures by returning NewToolResultError(...), nil — result.IsError=true, err=nil — a pattern used 214 times across pkg/. This meant the failure metric was always 0 for tool-level errors. Fix the wrapper condition to check both: err != nil || (result != nil && result.IsError) Add three tests in cmd/metrics_wrap_test.go: - IsError=true increments failure counter (regression test) - Successful call does not increment failure counter - Real Go error increments failure counter Remove CLAUDE.md from the repository. Signed-off-by: MatteoMori --------- Signed-off-by: MatteoMori --- README.md | 38 +- cmd/main.go | 194 +++-- cmd/metrics_wrap_test.go | 127 +++ dashboard/grafana-dash-example.png | Bin 0 -> 739243 bytes dashboard/grafana-dashboard.json | 819 ++++++++++++++++++ go.mod | 5 +- helm/kagent-tools/templates/deployment.yaml | 5 + helm/kagent-tools/templates/service.yaml | 19 + .../templates/servicemonitor.yaml | 23 + helm/kagent-tools/values.yaml | 9 + internal/metrics/monitoring_server.go | 69 ++ internal/metrics/monitoring_server_test.go | 268 ++++++ 12 files changed, 1516 insertions(+), 60 deletions(-) create mode 100644 cmd/metrics_wrap_test.go create mode 100644 dashboard/grafana-dash-example.png create mode 100644 dashboard/grafana-dashboard.json create mode 100644 helm/kagent-tools/templates/servicemonitor.yaml create mode 100644 internal/metrics/monitoring_server.go create mode 100644 internal/metrics/monitoring_server_test.go diff --git a/README.md b/README.md index ae07bae5..4b1c5185 100644 --- a/README.md +++ b/README.md @@ -188,9 +188,21 @@ go build -o kagent-tools . The server runs using sse transport for MCP communication. +#### CLI Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--port`, `-p` | `8084` | Port to run the MCP server on | +| `--metrics-port` | `8084` | Port to run the Prometheus metrics server on | +| `--stdio` | `false` | Use stdio for communication instead of HTTP | +| `--tools` | `[]` (all) | Comma-separated list of tool providers to register | +| `--read-only` | `false` | Disable tools that perform write operations | +| `--kubeconfig` | `""` | Path to kubeconfig file (defaults to in-cluster config) | +| `--version`, `-v` | `false` | Show version information and exit | + ### Testing ```bash -go test -v +go test -v ./... ``` ## Tool Implementation Details @@ -243,6 +255,25 @@ Tools can be configured through environment variables: - `GRAFANA_URL`: Default Grafana server URL - `GRAFANA_API_KEY`: Default Grafana API key +## Observability + +The MCP server exposes Prometheus metrics on a configurable HTTP endpoint (`/metrics`). By default, the metrics endpoint runs on the same port as the MCP server. To run it on a separate port: + +```bash +./kagent-tools --port 8084 --metrics-port 9090 +``` + +### Exposed Metrics + +| Metric | Type | Labels | Description | +|--------|------|--------|-------------| +| `kagent_tools_mcp_server_info` | Gauge | `server_name`, `version`, `git_commit`, `build_date`, `server_mode` | Server metadata (always set to 1) | +| `kagent_tools_mcp_registered_tools` | Gauge | `tool_name`, `tool_provider` | Set to 1 for each registered tool | +| `kagent_tools_mcp_invocations_total` | Counter | `tool_name`, `tool_provider` | Total number of tool invocations | +| `kagent_tools_mcp_invocations_failure_total` | Counter | `tool_name`, `tool_provider` | Total number of failed tool invocations | + +Standard Go runtime and process metrics are also included (goroutines, memory, CPU, file descriptors, etc.). + ## Error Handling and Debugging The tools provide detailed error messages and support verbose output. When debugging issues: @@ -258,9 +289,8 @@ Potential areas for future improvement: 1. **Native Client Libraries**: Replace CLI calls with native Go client libraries where possible 2. **Advanced Documentation Search**: Implement full vector search for documentation queries 3. **Caching**: Add caching for frequently accessed data -4. **Metrics and Observability**: Add metrics and tracing for tool usage -5. **Configuration Management**: Enhanced configuration management and validation -6. **Parallel Execution**: Support for parallel execution of related operations +4. **Configuration Management**: Enhanced configuration management and validation +5. **Parallel Execution**: Support for parallel execution of related operations ## Contributing diff --git a/cmd/main.go b/cmd/main.go index 374d7eea..943b7db2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,6 +8,7 @@ import ( "os" "os/signal" "runtime" + "strconv" "strings" "sync" "syscall" @@ -15,6 +16,7 @@ import ( "github.com/joho/godotenv" "github.com/kagent-dev/tools/internal/logger" + "github.com/kagent-dev/tools/internal/metrics" "github.com/kagent-dev/tools/internal/telemetry" "github.com/kagent-dev/tools/internal/version" "github.com/kagent-dev/tools/pkg/argo" @@ -25,16 +27,19 @@ import ( "github.com/kagent-dev/tools/pkg/kubescape" "github.com/kagent-dev/tools/pkg/prometheus" "github.com/kagent-dev/tools/pkg/utils" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) var ( port int + metricsPort int stdio bool tools []string kubeconfig *string @@ -56,6 +61,7 @@ var rootCmd = &cobra.Command{ func init() { rootCmd.Flags().IntVarP(&port, "port", "p", 8084, "Port to run the server on") + rootCmd.Flags().IntVarP(&metricsPort, "metrics-port", "m", 0, "Port to run the metrics server on (default 0: same as --port)") rootCmd.Flags().BoolVar(&stdio, "stdio", false, "Use stdio for communication instead of HTTP") rootCmd.Flags().StringSliceVar(&tools, "tools", []string{}, "List of tools to register. If empty, all tools are registered.") rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "Show version information and exit") @@ -92,6 +98,11 @@ func run(cmd *cobra.Command, args []string) { return } + // 0 means "same as --port" - resolve it before any server logic uses it + if metricsPort == 0 { + metricsPort = port + } + logger.Init(stdio) defer logger.Sync() @@ -134,8 +145,11 @@ func run(cmd *cobra.Command, args []string) { Version, ) - // Register tools - registerMCP(mcp, tools, *kubeconfig, readOnly) + // Register tools and wrap handlers with metrics instrumentation. + // registerMCP returns a map of tool_name -> tool_provider so that + // wrapToolHandlersWithMetrics knows which provider each tool belongs to. + toolProviders := registerMCP(mcp, tools, *kubeconfig, readOnly) + wrapToolHandlersWithMetrics(mcp, toolProviders) // Create wait group for server goroutines var wg sync.WaitGroup @@ -146,6 +160,7 @@ func run(cmd *cobra.Command, args []string) { // HTTP server reference (only used when not in stdio mode) var httpServer *http.Server + var metricsServer *http.Server // Separate server for metrics if metricsPort is different from main port // Start server based on chosen mode wg.Add(1) @@ -170,17 +185,40 @@ func run(cmd *cobra.Command, args []string) { } }) - // Add metrics endpoint (basic implementation for e2e tests) - mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - - // Generate real runtime metrics instead of hardcoded values - metrics := generateRuntimeMetrics() - if err := writeResponse(w, []byte(metrics)); err != nil { - logger.Get().Error("Failed to write metrics response", "error", err) + // Add metrics endpoint + registry := metrics.InitServer() // Initialize Prometheus metrics before starting the server + + if metricsPort != port { // Only start a separate metrics server if the metrics port is different from the main server port + // Create the metrics server outside the goroutine to avoid a race condition + // between the goroutine assigning metricsServer and the shutdown handler reading it + metricsMux := http.NewServeMux() + metricsMux.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) + metricsServer = &http.Server{ + Addr: fmt.Sprintf(":%d", metricsPort), + Handler: metricsMux, } - }) + + wg.Add(1) + go func() { + defer wg.Done() + logger.Get().Info("Starting Prometheus metrics endpoint on /metrics", "port", strconv.Itoa(metricsPort)) + if err := metricsServer.ListenAndServe(); err != nil { + if !errors.Is(err, http.ErrServerClosed) { + logger.Get().Error("Metrics endpoint failed", "error", err) + } else { + logger.Get().Info("Metrics server closed gracefully.") + } + } + }() + } else { + logger.Get().Info("Starting Prometheus metrics endpoint on /metrics", "port", strconv.Itoa(port)) + mux.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) + } + serverMode := "read-write" + if readOnly { + serverMode = "read-only" + } + metrics.KagentToolsMCPServerInfo.WithLabelValues(Name, Version, GitCommit, BuildDate, serverMode).Set(1) // Handle all other routes with the MCP server wrapped in telemetry middleware mux.Handle("/", telemetry.HTTPMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -229,6 +267,19 @@ func run(cmd *cobra.Command, args []string) { rootSpan.AddEvent("server.shutdown.completed") } } + + // Gracefully shutdown metrics server if running separately + if !stdio && metricsServer != nil { + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCancel() + + if err := metricsServer.Shutdown(shutdownCtx); err != nil { + logger.Get().Error("Failed to shutdown metrics server gracefully", "error", err) + rootSpan.RecordError(err) + } else { + logger.Get().Info("Metrics server shutdown completed") + } + } }() // Wait for all server operations to complete @@ -242,47 +293,6 @@ func writeResponse(w http.ResponseWriter, data []byte) error { return err } -// generateRuntimeMetrics generates real runtime metrics for the /metrics endpoint -func generateRuntimeMetrics() string { - var m runtime.MemStats - runtime.ReadMemStats(&m) - - now := time.Now().Unix() - - // Build metrics in Prometheus format - metrics := strings.Builder{} - - // Go runtime info - metrics.WriteString("# HELP go_info Information about the Go environment.\n") - metrics.WriteString("# TYPE go_info gauge\n") - metrics.WriteString(fmt.Sprintf("go_info{version=\"%s\"} 1\n", runtime.Version())) - - // Process start time - metrics.WriteString("# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.\n") - metrics.WriteString("# TYPE process_start_time_seconds gauge\n") - metrics.WriteString(fmt.Sprintf("process_start_time_seconds %d\n", now)) - - // Memory metrics - metrics.WriteString("# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.\n") - metrics.WriteString("# TYPE go_memstats_alloc_bytes gauge\n") - metrics.WriteString(fmt.Sprintf("go_memstats_alloc_bytes %d\n", m.Alloc)) - - metrics.WriteString("# HELP go_memstats_total_alloc_bytes Total number of bytes allocated, even if freed.\n") - metrics.WriteString("# TYPE go_memstats_total_alloc_bytes counter\n") - metrics.WriteString(fmt.Sprintf("go_memstats_total_alloc_bytes %d\n", m.TotalAlloc)) - - metrics.WriteString("# HELP go_memstats_sys_bytes Number of bytes obtained from system.\n") - metrics.WriteString("# TYPE go_memstats_sys_bytes gauge\n") - metrics.WriteString(fmt.Sprintf("go_memstats_sys_bytes %d\n", m.Sys)) - - // Goroutine count - metrics.WriteString("# HELP go_goroutines Number of goroutines that currently exist.\n") - metrics.WriteString("# TYPE go_goroutines gauge\n") - metrics.WriteString(fmt.Sprintf("go_goroutines %d\n", runtime.NumGoroutine())) - - return metrics.String() -} - func runStdioServer(ctx context.Context, mcp *server.MCPServer) { logger.Get().Info("Running KAgent Tools Server STDIO:", "tools", strings.Join(tools, ",")) stdioServer := server.NewStdioServer(mcp) @@ -291,7 +301,11 @@ func runStdioServer(ctx context.Context, mcp *server.MCPServer) { } } -func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string, readOnly bool) { +// registerMCP registers tool providers with the MCP server and returns a mapping +// of tool_name -> tool_provider. This mapping is built using the ListTools() diff +// technique: we snapshot the tool list before and after each provider registers, +// so we know exactly which tools belong to which provider. +func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfig string, readOnly bool) map[string]string { // A map to hold tool providers and their registration functions toolProviderMap := map[string]func(*server.MCPServer){ "argo": func(s *server.MCPServer) { argo.RegisterTools(s, readOnly) }, @@ -310,11 +324,83 @@ func registerMCP(mcp *server.MCPServer, enabledToolProviders []string, kubeconfi enabledToolProviders = append(enabledToolProviders, name) } } + + // toolToProvider maps each tool name to its provider (e.g., "kubectl_get" -> "k8s"). + // This is used later by wrapToolHandlersWithMetrics to set the correct tool_provider label. + toolToProvider := make(map[string]string) + for _, toolProviderName := range enabledToolProviders { if registerFunc, ok := toolProviderMap[toolProviderName]; ok { + // Snapshot the tool list before this provider registers its tools. + // We need this because ListTools() returns ALL tools from ALL providers, + // so the only way to know which tools belong to THIS provider is to compare + // the list before and after registration. + toolsBefore := mcp.ListTools() + registerFunc(mcp) + + // Determine which tools were just registered by this provider + // by finding tools that exist now but didn't exist before. + // Record each one in Prometheus so we can observe the full tool inventory. + for toolName := range mcp.ListTools() { + if _, existed := toolsBefore[toolName]; !existed { + metrics.KagentToolsMCPRegisteredTools.WithLabelValues(toolName, toolProviderName).Set(1) + toolToProvider[toolName] = toolProviderName + } + } } else { logger.Get().Error("Unknown tool specified", "provider", toolProviderName) } } + + return toolToProvider +} + +// wrapToolHandlersWithMetrics applies the wrapper/middleware pattern to instrument +// all registered MCP tool handlers with Prometheus invocation counters. +// +// How it works: +// 1. Grab all registered tools from the MCP server using ListTools() +// 2. For each tool, wrap its handler with a function that increments metrics +// 3. Replace all tools in the MCP server using SetTools() +// +// The wrapper function: +// - Increments kagent_tools_mcp_invocations_total on every call +// - Increments kagent_tools_mcp_invocations_failure_total when the handler returns a +// non-nil Go error OR when result.IsError is true (the MCP convention for tool-level +// failures - handlers return NewToolResultError(...), nil, not a Go error) +// - Calls the original handler unchanged - the tool's behaviour is not affected +// +// This uses the standard middleware/decorator pattern: the original handler and the +// wrapped handler have the same function signature, so they are interchangeable. +// No changes are required in any pkg/ file - all instrumentation happens centrally here. +func wrapToolHandlersWithMetrics(mcpServer *server.MCPServer, toolToProvider map[string]string) { + allTools := mcpServer.ListTools() + wrapped := make([]server.ServerTool, 0, len(allTools)) + + for name, st := range allTools { + originalHandler := st.Handler + toolName := name // capture for closure + provider := toolToProvider[toolName] + + wrapped = append(wrapped, server.ServerTool{ + Tool: st.Tool, + Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + metrics.KagentToolsMCPInvocationsTotal.WithLabelValues(toolName, provider).Inc() + + result, err := originalHandler(ctx, req) + + // Count as failure if the Go error is non-nil OR if the tool returned + // a result with IsError=true (the MCP convention for tool-level failures, + // which always return nil for the Go error). + if err != nil || (result != nil && result.IsError) { + metrics.KagentToolsMCPInvocationsFailureTotal.WithLabelValues(toolName, provider).Inc() + } + + return result, err + }, + }) + } + + mcpServer.SetTools(wrapped...) } diff --git a/cmd/metrics_wrap_test.go b/cmd/metrics_wrap_test.go new file mode 100644 index 00000000..0b8ca730 --- /dev/null +++ b/cmd/metrics_wrap_test.go @@ -0,0 +1,127 @@ +package main + +import ( + "context" + "fmt" + "testing" + + "github.com/kagent-dev/tools/internal/metrics" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + promtest "github.com/prometheus/client_golang/prometheus/testutil" +) + +// newTestServer creates a fresh MCP server and resets the metric counters so +// tests do not interfere with each other. +func newTestServer() *server.MCPServer { + metrics.KagentToolsMCPInvocationsTotal.Reset() + metrics.KagentToolsMCPInvocationsFailureTotal.Reset() + return server.NewMCPServer("test-server", "test") +} + +// invokeWrapped registers handler on s, wraps all handlers with metrics, then +// calls the wrapped handler for toolName and returns its result. +func invokeWrapped(t *testing.T, s *server.MCPServer, toolName string, provider string, handler server.ToolHandlerFunc) (*mcp.CallToolResult, error) { + t.Helper() + s.AddTool(mcp.Tool{Name: toolName}, handler) + wrapToolHandlersWithMetrics(s, map[string]string{toolName: provider}) + st, ok := s.ListTools()[toolName] + if !ok { + t.Fatalf("tool %q not found after wrapping", toolName) + } + return st.Handler(context.Background(), mcp.CallToolRequest{}) +} + +// TestWrapToolHandlersWithMetrics_IsErrorIncrementsFailureCounter is the +// critical regression test for the bug identified in PR review: +// +// Handlers signal tool-level failures via NewToolResultError(...), nil +// (result.IsError=true, Go error=nil), so checking only `err != nil` would +// never count these as failures. +// +// To replicate manually: +// +// go test -v -run TestWrapToolHandlersWithMetrics_IsErrorIncrementsFailureCounter ./cmd/ +func TestWrapToolHandlersWithMetrics_IsErrorIncrementsFailureCounter(t *testing.T) { + s := newTestServer() + + result, err := invokeWrapped(t, s, "failing_tool", "test", + func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // This is the pattern used 214 times across pkg/ - returns a tool-level + // error with IsError=true but a nil Go error. + return mcp.NewToolResultError("kubectl: resource not found"), nil + }, + ) + + if err != nil { + t.Fatalf("expected nil Go error from handler, got: %v", err) + } + if !result.IsError { + t.Fatal("expected result.IsError=true") + } + + total := promtest.ToFloat64(metrics.KagentToolsMCPInvocationsTotal.WithLabelValues("failing_tool", "test")) + if total != 1 { + t.Errorf("invocations_total: expected 1, got %v", total) + } + + failures := promtest.ToFloat64(metrics.KagentToolsMCPInvocationsFailureTotal.WithLabelValues("failing_tool", "test")) + if failures != 1 { + t.Errorf("invocations_failure_total: expected 1, got %v (IsError=true was not counted as failure)", failures) + } +} + +// TestWrapToolHandlersWithMetrics_SuccessDoesNotIncrementFailureCounter verifies +// that a successful tool call does not touch the failure counter. +// +// To replicate manually: +// +// go test -v -run TestWrapToolHandlersWithMetrics_SuccessDoesNotIncrementFailureCounter ./cmd/ +func TestWrapToolHandlersWithMetrics_SuccessDoesNotIncrementFailureCounter(t *testing.T) { + s := newTestServer() + + _, err := invokeWrapped(t, s, "success_tool", "test", + func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return mcp.NewToolResultText("all good"), nil + }, + ) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + total := promtest.ToFloat64(metrics.KagentToolsMCPInvocationsTotal.WithLabelValues("success_tool", "test")) + if total != 1 { + t.Errorf("invocations_total: expected 1, got %v", total) + } + + failures := promtest.ToFloat64(metrics.KagentToolsMCPInvocationsFailureTotal.WithLabelValues("success_tool", "test")) + if failures != 0 { + t.Errorf("invocations_failure_total: expected 0 for a successful call, got %v", failures) + } +} + +// TestWrapToolHandlersWithMetrics_GoErrorIncrementsFailureCounter verifies +// that a real Go error (e.g. infrastructure failure) is also counted. +// +// To replicate manually: +// +// go test -v -run TestWrapToolHandlersWithMetrics_GoErrorIncrementsFailureCounter ./cmd/ +func TestWrapToolHandlersWithMetrics_GoErrorIncrementsFailureCounter(t *testing.T) { + s := newTestServer() + + _, err := invokeWrapped(t, s, "broken_tool", "test", + func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return nil, fmt.Errorf("connection refused") + }, + ) + + if err == nil { + t.Fatal("expected a Go error, got nil") + } + + failures := promtest.ToFloat64(metrics.KagentToolsMCPInvocationsFailureTotal.WithLabelValues("broken_tool", "test")) + if failures != 1 { + t.Errorf("invocations_failure_total: expected 1 for Go error, got %v", failures) + } +} diff --git a/dashboard/grafana-dash-example.png b/dashboard/grafana-dash-example.png new file mode 100644 index 0000000000000000000000000000000000000000..6ffe3117757517889eec030e4e658ad26dcd2711 GIT binary patch literal 739243 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�yY?;y&C^#9 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 h78hP2mz=;-kb7{1kIfe$`tP|ZgUg% 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

>8yHq)$xAR+6*>0%6nHeLX@e?0_6St)& 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 z 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

-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 z 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 z zq0nqg2yML4&YAhC_E76Qvp}|$l_wNxS>F1%`1dxr0_T=T-An2vXQ3_MRX9#9!2W6d zs3WseZ~M3pSV@F|TuWvCF0aBD&tb=Jq>k9R(zvNE-2OLCm#_n#6fHFUjboOf*(Yv3 zdd{%=dr&qxob*`-#k|=u$AanB=uY`@d2U;JLdWeQ{qNin-yRu9)`Kt4&lQUvizVNC z8u?#`bilFw;JJ78Zk|R8%J3MRxt%BX04zeCU%9BUD$nZ#{bSWCEL8& zF?_QJ!)%&M!}X~FQQw>!Jw8WgSNZaIoqDE2*O1ekB5b~@Z4(ipbG3L+G5JQlRLTM> z@np%91r4qHLz&=brh*GfN>;wmZv9n2{)mq1@#?z#r&uf zayrr?jVRC0FGajF2FAY0Up7b$OdC{J>JwKf_z!w|)3ieHSf+S>FTEd}9kVo+%#k`y z$s}QY^LbF&tk^E({+>2IK3n^y&8l2+?(&;N(lb-i0T+{O9X;L1P$Z*bfV^py!ErYr zZ2&8<8QgcEn721Ddg}mlvevefhh*iDAufUlob)Q2pQhO%1K0E4vSn2%j%SqG-*#i5 zoY!XhJIEpWhqO;*mDID&(cJS)NI-I@s;^-u+UBzMc7b5k(J;g9rPe(Y$IyS2F zx{U7aYT@%k$R)ohiVFMFztZvaQ!eb;H$kr$ak7q3suw@S`Ii){p0C>Bj!K^}AuJ7{ zPYNu`M6-`N7-C){L(4e7y{_IN;9o72$FrMmi^;n=2ox$0IlW_^syemQT^IC4GH2ILv+oSp<^HXt~enPj|he)W`*X&O%MzdoO5IiB|UB zxR!M&sX8VoK5LV&*6yZ+p!DuA%>WuL3%ld0dQnkCWe}^sf6>#0w}0 zJ-dm7EmpRBoEG4r9*5)|G!L8C=?i{2KdHf}+kT;KBVs5$83d6{IY3f92`-n9q`E&I zhQ$$4Fvx4M7H#c7PWrn2U0LU;7V?ydu602kpRP*+RI*@UBc9`rOd1*`-c~_aUno;v zrDV=AqW#KKO(x~6O!!qe?vm0XPqv%bp(71I-i%$ixc0Cqg$vg^>PfO@B^Z-rf(hrm z@-+hut5B@u#-@RzvGsWF(|_wQy};X+49`ggvhX!hqp=G096>BpNpQp&U<8Ar@pFL$ z_v1lEV*73Ii*7{e5Z#^oA!xx9Y7Z$0CMWz9gQdMe4u6`AY#a7Q@nikZ@@0NB*+P&- z`c}hK|76p9zYcD#;NOpGVXpd z@BOP^6!T;h=dyX!J|jC26Z19T2L%0C=U?7_5G7*vM`d)B4w*?Kb}w{hC^jr2$|d#} z1NO)5x$sMjmMcDq>FlyJ{&LpnBDUwv7Mka_UW&iwNeZPzJ1T5LEGp{2IqduGoGsda zN2^ro^*MJnKgeQ>4fOM%;!X!RRiZtu9_QmnVWs=wO35{JeUV`Hby+nhlwgA;SyZx{ z^~_63lh^xtyI~YzAkjQvc#+1`ia9W_N6gQf`?B5Ad(eCc{BKU444k}^g%aM=`gGQG z{=;2{f-9b~k;K}TG#>hcIIH5-Z62H!*+q~cAaZdm>UzTQ!(aOvn3lo8m;4a#$lkX{kVF3V~WDj5q-$Pl$*oj+jLg+}vGr_YQG)G~($BKd5l-4W2yXK6?d-v*60*=S z0kv~s`_u|zFY5TAZ4O*?1zgi5X5C0eDaeNz<8gk;vB5W5qWQ7kFxC8E^-}z}DzCHa z5u9Yvx2yH>i5(ift?>1AG<6BNRZu1nETK^t7`^k7F>VINMS;4nE##3S;$GmOSHzD` zzcIa@YhCqZ70({8ZdY~k516r0EX>T)Q%&lCCW!@{$t>r$$9iS;GCDt_boMHb^TaJ$ zvTJtsu}cl+Vy>AHe$<1+rM3{GhSZR(Ua9=Yx$SfPUikH(wN_!8y^?293 zL-QK|cgy%6?)DJ#-I@;OHUn1wF4c#p9F<9AJ!NfiDPN>UPk%)W2g#7BXN+Me^@=*n zkeXhw#(Avy=!cLW9&>`}Z!0UV6Z)55!6Sy9uNMtl~SkmUoqIDBlVj$epZ?LqM z7jKreLvq_7rHf=+-?L#cs1S@cvj*+WcY33mbd4P)tZTY0r|e&ejxD?IB);Tp#V<^i z*-()$)zMV1bklsj$o8M?HF*aV;Gj4|-);1W=T+cXNyt@lbapK*I#H7e;)0^$5#@y7iyD#oC66A|} zk@=qOKnZxo61+pWSuadj;~p^6OhX+JC9N4UMd6devqPR9RvlS}dRhcg=a;_9(YDn& zo5=_*i$r`fyDy6|9uq!2fyLScmHoG zQb!*Oz8kSLq2Nv>$AS;4`b}813K{yohNp4V5IDC6LK?qy$3p5_Q8P7;E4}U?!{Nbv z7po|`WjlYy7kKL24lPPh76aU2PMBDb!|>yge6QN(ZcmNRadjvuX&es2L`4wUIh<+^ zq!jrnPQOH69fJe?rAu|&pin*XL(Pw&*x}}mv`9vA!{ea5NZdE3(@Rv$4Xx>YYV4$Z zNC?l5a&<&+sG2O0x+iiR@)1#JXL=UCmROG0In8v`cd<2n6d~ZRC*HBk)jE92Q@#6l zq3EET#QY0{u-3_x34Pf--9zi^alE$uU{ z_kRT$f)O4`(kziuLrW57+|S#cdUJN9Lsj_aEe9Y~|JHDR+$2!DFEMyZT%L}Jc!kjN zLfVf@NtwQv?7nfBoh{Fz;$|p0D;_RS_27RtE4HFIKFj=s4xU*XFOiQsJM!++-Iu+( zTYK?&O0<$Xm<{L1lb`D0Vg7+6WJZGeAG3+hMX;rw_8EuR9Is%9B68us{I|FlKg6-O z-$3r0^Lir0A1g@p&@_`$O#(+`^r#Yo8l;|@(`3_d*S>2%p`HH&7Cv)!RvQ@#xo>G>a`nn7Y--(TUpLhvtV_ZLZJJS!TZn+7Ctb-aKN;@Fg)a$ZkHKgDliUc{G;a`>fJD^a5Fouw8|FF;xsC zKy;CcG#>cshk#2ZKqwr>{kC}d<1=8|LOqtZlyda#q;BbTHf*u!mXpS5EbTXUN~2nS z4@K#hrzA}kHE{#uYzZTI@Z6Lk+kfyCIh`}ON@{M!yg(RHf_b|=syWZ^K(8x^!%D=k z1qvWr1c(Zj>9@VDoSo$pCUxF_2X=}ur9?<^3Fg_)8_yf+fz)8A^N-QC@_SSf9BcXxL$?gS_vG*BS8^QYh5XPk5K-*N|GWW8(6IiG2@ zt(!>rVy!{YG#i6vAht4#i+Xr2Qz{8nOI)kVUM^`>`;})Z7j{X!V?A6ymi1&(tW>qI z2dOS{MFl;7HjK5W^P;9wD}(+>j#ww$9qCv9`_uIrMpE`8hte;LZIFjJ)6>Hl{Mf4i z{a)B^ov>^jVYb@1`73e})SmQP>K31!a<+P31x0(Lu#I5fPfYXXV}umjlR2CuHNIf3 zXQ~iyM?+iTZ)Pw|yHeT2LlG;4YgM!(chAjel&HC!S=t4rH`mt^^K@37@`N z|J#2~)GMLW;j^Ui5X#f9ndOuY^^2P`XHq^Y`0i^dO71HJOy6t19FQJfP6F?dK=Upv z1gb3${P?(vMhdCXtu~;*k(wztm0Uq%URFo+;TLou5!|(5SNweokH%}1#usQRBPUOc z_uHq?l^61jR@pHtY*c`>#%)t6xglX44>ir*!R<{$V-}56+UcW=Ce&RRMLQBq@=gIE zuY;swlrbIq_$jMRkLkwGtOKtuGVo)tRY@k=wHaBY&vezKHm@d-@esxc#wZ z{FN6kzlRQewX@;DcYI@bPL@>gl(?rI>S~OIBy$dLLyoz=i39qIz~P8m1mhVt|BJtc zb#b+^do&0~kE7Gm^)%8+CFiGb2Lr`GHUWViX7r=#FvFPU0qQj?bu+dfi_mMX<>A6BsVf$MSWE`k!bcM8Wat zYezjFccKOM{A~I=gLk~oHM}Ocx-I8o&!-*!678;4tq>h?R=;}-cR;^i$01Dx)tpS< z=nWl=JU#d_ukRuC-1|8&DTe^H4*pi01aovoAQic7sYA8*jScGX#aC))b6JFd?3X?J zAM5Ot*9ennNBo_w$iWo$1^^~@^XUd;%Zl0{+q)R!+(hQj(Itjpm^f<@LU^ohU%}S~ zkPh;%NxI{q1UR0>JAQkX9-L>pzF+qn7FSg%bG1fTNj>hoRzy|S>c;3(2;b2;&6dkp zAfCS1`RTxD+s42%8M-iV>Z1)0X@VI%es; zSAtQ35yr=Q|D5b!ZZdqN^}a)~Q_uwKZar=!I2A$wlBr>2hn{bA7&Hyb|4oW8RFC15 zs&hkk;n8a6c<7cm&biPF)XbirdqrHXs8+A7Sn(;-2Xtj5EgJDY@{N?i#-*;UNXSy5+kJhUa z&&~_ytsyNBA*oUi;yefiRW_Qw)&x?CSff9kzp}YzJmxXp^h-=rO?@gr^jz3)31!w< zoAW?@?S3gf+1Ug-Gk(*yj=o9?#=!<^?zTWpQg-k@X9c(*U;OWnO8FQ;7vAH}EK8z* z^u?t~ewW=cN%0p0hQJVmDiTF#y;3lWYJ4vkMTnhTdH*Ov*C3qhP>lDPXp%hJuYegYXgd5-ai*-~5X)5o;9zB83CNss`SwrrC_h3^2Ld zIvL-u{oFlN^Z{>(-myko{o`FF7Ji4R?1RvL4!mi`V<8^er}aW{r@(i(lo7yx$_!?& zkzZ#~eA=KahA9HA6Zbes-#T_mAuyrMo)zFVfKY4Ar^xfg3h7zG1{CqrI*@zcz0G8j zZ3Y`*n3bVU)j0kUh;g|$rDdPnoD{z4;}>w|w*!*hxz%}98Kf;q>U1pLVRp!Z)Krq8 zzZ@9y9BNN!J-_+g8u;8z05_f?p+!^X&bGoc7))V)8Ur0QHbc$Ah<%n%>$ROzu4Vz5 zzy zhIP!>oJ$~X>t>pP)rZ~W#dv@;1s#?wvXX2pmK}7VMhHOY?LaL%9H)Xo75LCP-)4`D z8gZetwUn)DQssY6?zr6`=_fIIt56FeAg_Xpe%_Mu0TrerOMsL?mSr-f7vt++e9Qfy z&!tWo`NUA8*@T3j5(jR#DoK1WiY&7q5;y7vCxP9*D#^jOe~H+3jqhbix?YcJ4j!VA z@u*7$reY{l4$U&onMlhg{g>(h>*=7GeFwZHn^zKGGN z5WJ5pq%#r+vNb~aElb#W2M{Y)s~1I`g2PjV7ky=?5Znl$VVCpFawZ(DKQXy}FplZ0 z`YXiE)FgAZDdcqrb;x`%wqCvbY`QPxone$iCe%s6|MXrMVa|XAv;nQ5Ycmj{Q(WdW zE&M`1_)cs`XpTyL8%6Sc55>8atpVdie5hS7_KC8oM_wj(v{P^pAq@aK5smPWwog=cCl z%U}H{po`Xu*&W8YxC0g;js0+$dj~SJ)m`NLV9(Se;?O!ye*eYlD(hqLmAlRW=v+j1 zZxQ0#Rp3yRoGoS{{v+Pvi&;4^POm(KP>jyV5gKhQH9p1d*D_TaG_Dp3S`1>d<`Jmt z6WU-PzH=B|scJMhyx$RJFJh5sFSZ=X<_X%YZ z5~Ii!YdpjRv%5pM04JSs5jHaY(cZ#hJ`NU@k34lNxcq(mWdvcT&Ao6&@nGQK>QaArc?8TijI9EsZGyg__x;NiOBm_M9#yZEKiWEMARC5Z88y}dD|S;~AQ zo`}s{1{AbfPQ6?G-TE*y~JcJCxr4 z`<(q6N)j8#D=tseda4Dt#|AC^A%$($QzdPIz<*1Y#Sn1qn z8zo`x=6C2_JG1y+WJlGr_6=Y}D*A)^X+30I3=iYhz?yE^q`HWn-Pz{=m5}pH1bkq- zomBi+28#NQ>JQnRuy&P66oC_2D7{7s>A%0k;>d#kWxOme@w=NFT8Wim5*vc|%3^ z!OEM3bv(sG`^;|zL`ADX4K_Hz9+Brh?29(;1<6nrCxiA?wrK<-XUJv9q&UjfFs^qMBFwjgJwmuAmE|yBCC)f+*JTW2(4D48+3-L{rgQa8Z!%Wd@J$hC5Gk@ z->pACThdjGg@>3e(AKcpE&G{uWx-yDay(J2V$h-eiP{o^<}k18%*o6OkgB{jj#IUO zqTaAjJGHbU{>P;?T&9q5|XZKu_~?nAOa@Kn0sPCiSK1$&D|+Ld2g5%J(mg!l*#SeJatsW?I^HfLa3PrS8^!=zBZj*F(M$ z;v;;&bB9EhEH{Q)a_qY7QR9?YLoS$vR&kcbLvxV6F`pSLgc-paK}!k$MRG<8arZDS zUYf(?Vi>;Xc?F=e^NE*!nEYz6SJ|gB20k=VP zF`U-}6I8pH_~p=krkeUzQt4}a`+I!HGy5~&iE6a35$b{>0^+5qbgbU8lon-SFH?Hi zCU*%PVt;suhQAhYzH*|F`&xFOFj<>}@NA2(6)$6lP}P9ZePzz&UN$F*erG|4)``uj zkJo-?YGa9D=ZlrPZ&xvTcL(*b#p8;%sCW)qB6`!m;fPEv-w@hxI=_kc>qbg#JP1I! zxdYo>I*&DZO_*{Mi^l%jc6%JsKgo@X!PKZKr^JY@7si#P3(jG|XDT|9?B5A&^-OV3 zrh(B{C**Z0u*ftsozR`-Ef0!oT{VjycP?k{)jbd5i5{r{z{CVq%|LJ`sCmkM%S-1g zw@YRqNimv~x-{OD&52Fxbf`8yMJBSJs9WLOA08_XR|uoffuM^}y}ziK>HBJM*s;8` zo0n~~oH&vR9DSCW2oyxW{(<+uVN?0ul^Wx%qko{TFW`Bt$qq@dwkP%FRhpjbRf8m0 zmDm$G;A#V?%bnqzM(8c)+j8WWjDK)7W33wMmuOdhZmzO&xhwO=-oKxejp>{iEFC5z zOq832qoq>Cy1evrF6vKF0z?`L$%Vv1&_{sBkB<&XS%CXh@+G zhur_k$$nH=gH#Gd;1o9(%y=wu#e~^e>x=ARyS2f{Q&5aK)ow8<@9r3=g<4)DAbVg54HV;l>ho$iqKU@X(O*m+fmme0I*Di13oDkkgl-#*; z7L`Yj$LteIqY+tUyQz8I?wud#g<`fQ-c0+@03ryfqFybDliRcdZmWA-lS6#&Pn8Y9 zdu&kS8Q*ou#tTvsW+wy#@Y%&o9E^+cB(7i%wK^_{ViLmhn}JeH^U33e8vw|MW8Q!; zlAP`?3SwjVHv||<2hrI=tlMwZ<0mDyux9u?(dwHmx~51gHpkT!X)Q6+I+`OO+tui} zrwWu$lb?V|k~2y`-S&v9N?85AgNgHROC1)Iaz5u1yi;QHNbypQzhr(l{_*ZMH-!SP z8nmJRWo0T0XqiaPeD5DU^vkd%vyt8B4UzH;d~06p`~rPP@Y+h}kjD6LPrNf-M>c(725dLT) zROQ{u1PFH)p{kWs+@SY+@+kquA5R>#BZctUHXP#7)x@N4G2+&erCx@S23{8gxMyqn z4SXS9CvSDXgYzX&qVcv@lv$QV4VlCPRbN7v%kRjxK~ll#$GaG~)s8xAXur-o{UqKu zIt4zU`s^>1T7(WMgRZ zJqHyLRKOI$5oo?5dA@`?&curwxF>(}~_c1kPMY|AUwFVv>e92i#Puw zoZFh~s3Oa)0LA87z3pG1yzR|Njhz?;P!*4VYz8Si7iD+L{kWzVv$r<*8vL0{v;!AJ zUmYS7ixPD@OE()Bki$~FQ!(f-4pdQ?Z#r2d>4}Us(x8Q%R{>S=`47Ss;2WGd5M4&uPr{7LbMl+ zV;_-XFKcEG;IIgrp_0Z?5D~p=6i460s;U0{(!5xxPrSgB-ZO`6P|x?^=olk4b#yu+ zKk#XaA0FMYdG6q-AxF%0msm@LKE1-79VAxY zWo4%wD2~sX9mtuCl#;k-N+MgllXF<`b}fYG6&-_B=O_>T-in9MrUFaiS5WZ(N5T^J z1yRjN$&YHX2bGOH8C61_0TH8KmFL5c?=;{& zgJx;ubby0(HxqiqKVjt7P=mQW#Q9EvwBmo3mH)I|Ek^*icV$&5w?9#z(^p`uNA%|q zndr|*pzV}qeBhKPs*?b_s2P_3P3(2)c!sOw0D-5VOfA$@5kEhR?RjaWvzxeQ*)?C8 z|1ol5ITe??Sz=JZr`SLb?AvL|Gh;!wsl(Dc|HvGtNQt4fB`Oycp`z->i@RpdL;6MH zLJQ83TML*kOR@JS%aGcf6f67+OgrBl&?wsHF~L%e^iRJS-w(wOK23hB;Dy{hTF(v5xl|EeUI`1i*S z@uRq)LPj4KdOr`O1x zNkMgC629F7lfks5=@M&CmCf6aN2~nakAF$|PD_L#*7qe+>{3`oN3+2fNsud;s+)E} zqwrEAyxbsB_hodxqbO0EzZd=Ns3C*~$>des(y&h!RPn9Kl{_w+D!`9l;pB`fdSu_M zx1gZii3NO9-IIro+bB@05O0EIJ0S3dCClrsekBg)oygbETjiGOR}z?#MmHn(e{08x z6G`3}3@&En7S)j=(QUIePJ{dexpi{fUh$~fRT?!;Ep8}$L!0^a@Cl>EQ*d+@6 z!RVnZjUmnM2ll+=2GMwN#Gk-wT>F-J3H!d9%JZ2z&;chPJ~T7uD)>`>_vqShd1f_| zzF2)DJ-;<9Xbn@OsKsBZ{I}>;q&|D%8KfXxAlFZHiIVKpYNw6HaE^Y@Yo(zNZBO3vbb1a~KvT-_RLL|(@!j3Wj*6`0 zS#PFFoiSDt7Hemc*H>%VFR&d5@=+*uH(kPSVKK)iF!i}Z#eV6mE?Ixr537lbM6Iwrn8=Q`=LZjG7on~vlvSo90(L7NI2Nt#>vK6mZOHC0xUz-f1KMo z3MLzbaKEN$I)!pme*_ zIHz3nR*~0iKr8C&Pt}~(Doogw-)#k%lnrM=g#=64*pdoCe+4vMb$KHr2I{|dDITr; zLPPEU-CJB{ZQ3mT)Kb0uHQY}EOK7Z@9{8nIB`n%#L=%LYN@p!`rpp4bBb4XbLZJBk zNxV8|)5a8-U<@fr5@`}VWqcWup5wC}k|U&yiyrG59)~i0tN-#-$v{XeOJ6GaE_(Hz zpZrqDkctFw%#x|dk$f~&K~V)iK>ZvrZI&_XYG z0WbV{)v1-Z_|uvqGjS&>uJ3b_zC{YP2$X_5PZBE?=p#%o zQh;au3tHZn32VcC`_RV8&Tt4LmtsR2TUL8B_%ixoiBE*V@6CpvKDwr7e}x>FTFxgp z#K`>()lF#s#Ld@(8d|j12$4~-2r!l4FcMT9aIKokkD)j;WV9MhA;ra?9U*;V{x$Mw z2_hOLne62VnYDzWo0m3ZIVeG!Zloi9Ji*OuQvdS#pzY6HI5cIv8rqE6Uih6SDG98^ zWlIe08LU=qxc)KI`^gBlaSLkh3OcDgkFe`qw9chETL+X(4M_?NA>X&n2k6UaX+miN z&HJuVHr>XFBY)%;) z5(#ZI9sW^qscW5oq}AX-p|}I01hUg4{XWo)K;Qp00<(GAbsZLnED1-s4Cn0qKidcLAFAK8rHbN4|j3Pc*BIzYLwg2lhCS|x-SS37HiN71vz6!_S%|qfU zKxPDF4Pt#wFpF*>+g{JEnOhTizuK`oceQwXwu+D*+%1Nubv z0%m4={Hg!FfukfhSPR;Q2qS!f_SJ^rIjj;IfKFjk_to2pg-``c(T>QXm}EG-=BJfd z%cb0uC69I6YYGO1>kL6543DO{gux6sgsjelTD%4O!`KV>Q$D@x0>$F^2x`V7=o=S9 zKfZ$k1^?RfJ=91xoD9*MhtEo`wLT5_tUbvaaIcCywur8wNSnnry^KqmG)n`iO}}s6 zUN&D0b>7ihvWPM6-&FAg)02ml1}W(nJHQ{U@T`Q4EN+gb+=~w7dRtD!ilNbSdRy7? zkhQ&{_*^ayM0#nK1f!L1`nw#zQ`m8z-G8h>6s&FmSW7$J^uLbXks>cxZ%47e^YD~b z4hrUpqEA`^$`<2-WU*q+n9MHjmGEA}_dKE18&J=gjzz`8!H|zbMbpa9@$a#oS8Y=) z(!_)u#kQQK1IKqDLU34t*cKL>$Z08W0UIdnU=~9q@{Bro&t)wTX%~>OEyGFtp+H_~ z{6|E!Nu#A#m@!iaC40m5E3b1D?Pe$NN9Z%QvM%(Esa!_IIE#miM;6=jSFe&QXD7&* zcBJzm);6u--NW9cB${;+^BaP}H*c=lAQ&ZIo1S#L18~8hDQdY*vo6#N9N^OcUdPdl zhZa-N(`KxRUZM>$_g~$#Zhxe#a1YZH?wPhlL}s|4gB{>>eR7?lmVi)7yXUmZQU%DM zT^>3(&Jaxs+39s_yixl=uO!K(p9Y<)iWuBsDk7NG%r@>JDnijxk9c9jS4AjS3A-&T zgs-z(UXj%#?d(BKYlOkLPF?wNso~Jae7!xJX$8>{!p4e(GP~$N-lda8qinn_ zTt6@SwFCS8zvG>yeF;fLqUa@b*ImWO;*?f4jZ^N|p-wj)o(oJM9o0IM9^0qc5$X@9 zP$eUC$SI(Wk}^e7k$u*QOn=Z4>Gs{tC|W`{O#mlJltGB-Uj-*OF(IZ!w2sY&eJ%vDe|nIcAm?b$K9x@-Q@q5gm+5&7=v{OIDhWfc`gCk= zv|bocV<232UgmOAdm^;Vipn=R3f~HRMq5>#OCp!?o3i-9YBPy?BU*`sR(iNxJAsfg zFI&-6(&WL1WnlgHiDH#3FgMqvl=LL zwOy-+eu|*zG^djZ{oJb~vY{5-i5qHQc^a!9_2&!1d-MHrlPnDsn7?1WpnHg)i07U8 zFJn~pQAK~wI~+#>s0~Y5&CNh+;f`v4dU)R=Irs+~3=0oWmgHCIMPFA8k)R(m>ZOYj zQ6#dYIxp-0I8b}ATmx7$BDT}`0}cP7vJ_o)3<0Lt?~RHw(P^7nPbftjCp(PHUM)>} zF%rem_M2RRjQ}A;pm1O;C*}aboDH=|5SMFAN33GHfG%+RJ~7??KVX(u{mNt~j=lkM z_8;#FN`i4H43NR+@+JCOK&e>YMpxeCqqcbq$j*O|A+E}B>?>oS5JZ^oZD%KIf6Y~R z=RyNpE$I!_LR%8io$?ypo3z?AEi=xF4SC6Yj7AO#2?T+l=KXGW znhgQ3=s_NV?(2~3tbZ4G!r+8U+v@LEPHa_Ca_Ce$fvqY9ZZQsS?D*qhFzxz{4sZxN z$saz5LXBn_JQ{@~#S5rfS{i*wY>tNJA3X-8rM{oW1>Sv`zJ*>gU(Y*zgv zXhG-8Xu_aBO9$U+%dDA-FSzOp+fl5nN3U`w&uqRV9A!yBqsrOnRHF%zyflsHk$NA0cg*~EquTs$%H#^L=Zqia zIM?OpuB+wcT4afF-iMq2L?R2rI<8|@377V}L0hePyS=Y(vs7*ydp zG$m;=&*iDfl0@g@w7|+W!CJ>bS~oXGZ!>D2#!nWtS(kYdjq!#0cAJ1t$nHwhWX?!2 z;-Db-aDq4dQPq&%U|fSxy;KICJ11Dz-T2eh z6x!{W={4}_^j{Vpnu+hSR21WX+Y9%O+`%%G4m?vH_O<$n3j&bF!VfdsMo8F=T#dSz zJ>SfvX%0Nx&mjp&749zeT`V;ix6LZm9$m<~n+53N=C<`1#(Joy#z`)CIwK}GAgLj3!J=~Z(qp+?C66_po;_PcY}C@Sn;!qC}mRL1C?3FYNT%z1!y9@|VxHDBJ_~L~XpW%H zn|=?o3L)|6PJXY|_Do<~G>Jn`RrFD9<`7iMsFp`78+8h=i%KbtcDXZW*BFxwml71n zg|hKqcZCAB0dxadMyDMiQ8RLW6Znc_aiW4VepfnR#EOTSbzKkG8@yUx!wF}DS&ZG& z*^!4_%`DtwP)2zDebL+j+<~4%E$WVx0>Z>U*j7wL6i_hNlJZ}CT>3gxb5=CUO~+QG zty;@FDV9(;^J#hE@!c4Qmv+t0k9myB(JLBfi@cG;OL+es6#k*|Z1R9rDWAiu^yGX- ztPz|?{PF*}si9+tp-U9@JKEX!-gBrn(ZM@` zkSOkZs|aK4f;Rra zjPD%u)+sU4{HYGF3L+0K2T_x`A|`LCg3G2t+V~O%xqsKiWc-O&iC)k1HTAQN{^CTc z%oaQpHo)gA8s`wf9u%uS9x`62oDfDjTJT`Bs~8I(EF|ov*aWACPRQrtTIosrtC2@` z%*98*7GV*YOmj=w0)RY9#WMFP=rW1GL7tzK% zmpZUGYT1cFL%nxm0UlzWpEBHZ2SBUVpW8qFl+^rylvtYSyix!}D3HGYgZ?aB^(Q#x zqw6nGa?w^1P|v!ze`F|?6Jae8uZ~JG?e^jPk_ZSBewhRW8S)9kbt53JG>2lijut(y z8&unR4FV+Zr}{R^^*IACyOFk+Q(v7xQ@%yp?owht#C{$HDuBaRCdj71$psTzx{6qBj1Vix}x;aHnZ_9v7%tUlrv zXd|RS>jH&>9kXRzE0p`yz&)lizNK9yoRK+L%X%j9}OaG%x_-bvJHnR8Z4c%Y3R?lDj5+d@g zf2MG#rsfF|z9_mauwOp?^6v`OXLLe_Udh~|olG2`dreR3ebOHZ;QBz!5&DP?8OVOK z6j8ExuJgTlV~Hr;B%#V7v47|?+pOt^c-&q|tds?>S0n9?T%bgqquIQC3=jPF5l~z% z3(!^Dn*E(@$<*1WQ1@2r1|Vk`=6Biay$d@hE@?}Wp7&AjTho@Vo8!>$YhgUk;AvbZ zf_h!^d0*<~L1Sc6bKAO|tUTq3QrY=mP^R3>giIU65?Ai`0CUi?H1@kOzA{$9}+Iv!{R6+|c~h3D`h9xs<=S95D&5y{nwuuE@$S>%KqLvgZIN&BtQ zSv1U@oYd-Ew2Zhq)y)0pXJDf6zYFJx-UaCbkL*?p(p9)Xm%+;C()FyiealshA#mKU zWN)%}nj{R*KR%9*HfRPybA5!Ty{|6Ux*k)Z?f~MbTG8BhsN@$H;-x#Ru9JY@zTTLm z!H1jomdi}(P(jt*d_#K^k-3}s*-q>gh`V|JEZuf{>>e2QgRV5jvcUR;$QN%KDyzc^ zb{v;Q{~s1WRhXz`X1mo)I!5WbJGK%~R6qV}4=Hg>CzSyT0U0Pi`rGeUUKDAdG+VEDOEZmag3)G62wyPR5P4)_*4>F@zj@ z+isCzZd{<7(UkiMi{L|hs$9(nnS;Zqi7Bdp!HO$+Z_BW}8-bTE{yROqlK}WfqUerk z_LecMxp!I$!ui^hmywIH;|UIUiC+Mudrt!Fn-5K~ACx#tX~3oJ(2}#U9@ax&*~pSG zs%AfKM=p2Vr~!p_V=Xa!7IGTtpIlccsO;AE{?phQ$0~6Tt*(z9ngK$vq9O+vT%Uhy zz7AI!sWIdaKxhCT2s&j1bKZ)GW_W4mFb=12j7`#Q5+Jwoqv8XcEc#CWbAJ@#vr9_rgRJv%&PIRlt<81?!`pr8r8a6L3$^O{AWqYg zj_auOgU1lanAY06ky=2%Vt9z>wg*KJuz6BJJcN*to@PN(!hzLosPm&9%kNcJA*`ze z)VdG0S(LYa9+O5?OSdOjEt5R2-DhX3;I|FF>?m3SmgWBW?XdUaOB;)_op{(q!uCV%%_*>}*hn6MW?}1zlFVM#(o(l=x4IZEo2A|9+GT4BuYze5dm6l8agUnDk2>6_ zq0w&b=PmEm(#L?&`%mO%c{zFPG=C0#C6m=tXz3q4twvH{8(ykt`}b}JpAqo;M#$2U zlYjwEl+mwOs-!{Y09d+i%L8{fiYQe!Oo}t>Qv~3H_HL31o1!vRNf4nbC1L?3wXVnY zi;n_QfROTDw}-AK%dx?o9sZ|~(YTqZymAP-Y=+|VmFuL9weAw4XBX|?pdMmjkMejY z&gF;JAj599{JX!S;FE8l|78*fd{X774uhsxN`k`5A?N?ujXHE@>Hx#(lrZ zlSPfrnM6!w5QpLi4u=rH?tik2vCT;u;2pWD%CrzbD9M-ZHi z>cMca^*MC-aM-8YJiueiEO$0TP@+P>=`-9o?DwFx&qJD?wm!QFTx+gU*cHVc!Kmnp= z;TQW-Ig-*|<1h~Ht1LbkzNZl!c|(Ad)U*Z4y%BiDzwHaBK3;x2yDh5@A(iDlZ$c8u zOOwsR$9$tTiZ6}6gh)6E8;XRhrM*uWmuLH1Ye&JW>%`go_QANUtO@_+e)_yD+PGb( z$eemQRBn>r3+;x@?;+@C@p8pYKCIv9eFv!SZ%>J~k=vBQie{~75zEskKc2Hk&`{s0bQNVZg9Ly2J1u_s$X0f{Xu+O-s+wW!Ca)@Hpr=`Np1!tk6nrGU;{`}+Q za>klmz~(($^K`>`RTG!(fjzhYg%(?eL?MYzFu2DT8pbWlUVe6|__nitxBC~2mGrzQ z_uhX`rwXYaW+f!O<9qC4h;FVy7D}<@2lDH?r9_C`daBmjoM5!wNl0ZPU4B`n-7-wq z3awToqlF zqwtQdxN>q3#4Q9_@Pc`6(FPM~e9s|FFLTHOsqpx|pVCKIZ#8 zW5Xv9?6A#*hH1^x0XFgd3@FfNQ|XVtTw?V!73sOAgP;D$G5_g4OZ);Jus@Ep|ITwW zgAse_66!qIU~YqOPVXLsi-@D1zUxGthlQ;w} zjkhG^1$WMbZ+X}>rxBFf^3gzH95)%tT$dbIlVofe4oR}<#oXnjmO)^$s5+WIGV-m^ zxDAOdrHah}#*CouRoU#eDFVynJH0-3{Dr+^t;a~SA6VYz*WKbZFcb8SLR-F}&kxmV z&TZnz{9rf&E2?#gRdx|(LS;@=U8LPBR1{s7X%NpA-6Ys)y2>4iHP2TP0p2vKvCWZK zj<&@2soHhHX5bK2Q&jW~Id%wJ4qm7Hg}68i?wR;A3i2WC)F>rh-=wCk%KBR$s?Da@ zWCD5=XAJC`+xU!ePfdOKhe!Tei4FL&Pzby-95RtVHwPk1S)DO>kx*`U?iu$c+Z|(8PQJ0!cEvjvBgCJFy&Whk&{<(S>f^pgqtc)v)n85g zg2qzDP51Vfm`^IZ;pr}>{ABhK(x>QMX1wl>VygqJx7aF`tx?XhC=vIQ&znfnlO}sT zbN=*P5}!%_l+clV5G_|I!Jq1gAQbanI^D%7Fd`YIvLp z4Hk`)fog=tJ_eyY6-F`}WkTBSQh)~;cf8Rn)jB_KLyvmEDhd=IZ{nsWlYv_V?pIq> zSkhD>G}hoT`xMnvs7_!S^1bbb3iS|*+1vDG@3~>{;u663`J+t{#2&Q#`~X&L3crEZ zEKNJJEKmOZ=XZa%xe zj0>+-eM-`^vUtc(6A=&IA$VFP^Cm&46@PfAZEWS}dCgL~sOS5<6uspA>Y>Ms3FGs} zG8?N5%Tc3Szhn7fNmx{?RX8ZTyZ+95y}{{OM*9z7joYAF&`FXz==v*oSbnBL_6Xtj zti2IOg4+`|=sr7FL$K=M)s9KNw&$D{Jd?BlUo1gJEt>W;ud=09$*g(jb_(Zt>1nKm zTdyCaR_nmbA3)T+>6TfxV@S2QP%CQn3UnWw6nw_1}ygc_E?@n_G`X_eoPpc7R_QJ^HRiQ!j#ObYrsV?kJ`+ zC(3~JZij~pcU?`FXbNj3oAd__y}5-sL7+JW0%U;4492ade(mk10vU_Y>)MQ3dzsO} zLU0*Ll7IMqs~RBK`ensfs`H{tXrJFnga}jJSOVN8yTEd8yefaLp(SEp*+krxbk zm##MsS`SJPUi1fUb~;3SdMr(v>Acb9w)FCeEmaWr(*$o(% zj$3Q{0um?M>d?9Nl<}8v#zDt&!la~qMkc!KX2hAiQQCOa0}_$4n0Ylxb!xQ81D+I2C4L|cuExpQh^5xvFtr z?>$=Kk{xKmxNjzHtih?>J5k_}p~HIM=41Hf&I9zB`UQst>{jFmgz3`d%^)@E4=Z%4 zQ^S+;J#igz5!heyg0j;({yc~G zAkTRP7YJ$W`LRW?+-$&Xc(2kv1&sRmEHusa z*aG63glzVDGEWt{S-PojO%*1l6>$#ng?^ilpXbbE`#Q`S1yMcKXjK-YvT z)(G>S+q9`5MbqM8DQ3(?Si%Qso!wtFXTWcM2V{F-xJ+4o+(t}_EGpXw=BKE1HV`Fe zoo-~VP7M=H;X(L4yy}BX0~Fg1y*!~^jxE_*zw*C638i{(uS{60$r9)w>w-NHrv!3s)S#TIcb^gP9%343Y z-8%$+ZZY06gBGj@i6ksM>|$Csy%fjBavEtKzC5%&-ef7pFDzoooN8pPx>IC|4}@ zdoawFht!A>{PlC&R;VmfW$tZY{T=UnwK(m)?)2mV=f`Lqe|~(dE-btu(xOtR8S2HD z=N)$rCN_I5nS;XQj;2akvpl?K)HBGN!C<>P%hAf@>w+E;0oIYbk_NI36@=G2hPwMM z!dmb6q@<%Bna#0nOjIm7mt4!I`f{1T1Lp+GW%bchKLx60(xY_V95w3=m-n&Qv}*Y? z&XX>KSg2e_v7On)zW}%*CZT`@d%%5h$7_BiMrN_ODN!`^wVfm2Hi}!`L~JN>op^2B z4mN?`_LAh$@HTl92XA`^f2mewjT{}_m{Cy^eHio{$$l*QI1Ufq{qvf1|C>(@z{}qk zccwUpRwaAt6Q*Z(uWPEE8K7H0wkB5iDb17`+>#= zUR3C<>5&M&85Qc$xSZpdC0_B+8ZL zd6oJZ>!pbiAnHnP;W?agBE0+*v1Ye)sn~SkH`>@bJ1sAI<>{zle3UVeJm7jRY>WZu zim*N;zU#f`^}K;kO(~rK*|>_3S*kBa;WSgEC6q88i8o{N~)^R?*c<)7d7ySF~8cGU#iQs|WZ z=p4KJQzpFp=22~k;R8+xE;nBHH~o-#SCY?$M5 z$0%F`V#1Qb@x%!tuQmu?K7qT<`?(MJkCV~1Zn8t$8&x@oG4RB{jz{CbUml=ly-See zU!TC=;#64xIuMT(gUQEf3EFX_RUfYbx)s;ckykV?T(1GS7eMpOZ=-XwAf8 z&2iutLhm6|p&K-)omku*aT2?G`>Jqhjd6Knfg@7l>O5&uz4Te@{0Cr}AWlQ2HVk9x zU?1nP9@BfsB7ZY+4CC=9)&%{&rz)qZk0TJUZ5>sJU8*BJ_;0@fEyJ?^Sk?DEh4Gxr z-}l$ePdz5}nEQ0IX5lB9gL%elk)m$)+Ww<4p~pH8mg_OG1H$fE3PAxum##*o4V?TT zq~1jizI!IkL!-T1=_?mP3R7Bi#A&XC;9dLXQ+0Dz90H=cGZ+?Eqr209Lxk}vfT+<& zoOrAN4&q?~Poi0FG??jIP>x+GX(`=+vbvWThpoknl}mro;`U*xi%~SJ44UWN%}6}^ zfxIfH5|cpY^QQAlA|(Ari9es=^RaI_lQ&0Vmw|658%*5MAsyQ!M*s7nCLavO#Ypuc zqX?1!Za_{SyA3>R`Adc$kO9Ud`|2TM`tBAU&o+hVZ}Tc8$ecG2u2|G5qSDnaEMOlz;goWMP9#%-PAlJmO$OcM1pE2! z4tF-(%b?l^rRAU1UJn8FTL-eUTWGu!;AM8>tmpWS``d-GibtWaaQ`7Gc!UzA;l~hS zn=eyxJOxvFA`Qo;++|%pEW-Ap4_MLTD7n$7EHm233S>1Vmmd-J0qveRHB{7v_yPCs z-1`h0e`LNU9xl+PYBhO|X{N({%`VC-|HN&XZMF%Cq;r@SI zePvhFU)U~y?jU{V_hIrM7!a4db#jTa%~7K?D@3q`p!Wj% zC-Np3TxC;>VtjT)6;njDwJjz)*Fmq=4Wk*v;FWSMNua7f&F7Hm$jfW|M%%6~-KY)g z6W#~0;S!6ct}V&{xbiz<~zprw# z#_JTIgeEYgo=Yof)hEVdGq`08476@*qwB2`JZ<#ZJ~QQEcbstaEhLWY&}`9s_MG?V zSWa>=Y-os^EASP+Im;zp_7>X~{T_zzXX|qz#$~cN{Ur}5>mloX90HDTpwlyP)qL%- zXn@`p_a`{LJh>k&qx$B&TrWD$8lo6&(rad8ub-LKZC%Mw^?s6P)N*W0<@Wqu?u(^n zaA4+qL`6Iu8gw;M-5~hj3H6Ma5i~3D$fe1$a(lHtB{{Ft&MOf_F@V4FPk2T|>F1kV zbSpT2%8;%r_F#JveKvTaJdC_7?khoJk}Y9wFN(wTwJ1we+#O?LgfqCiblm^lc#c?_ zS#EFlXnX1I!R9lPeEJBZ$&Src$sAxa(k~?G2j17d*|eMJ67yrxHnRhF9;=Ht&fAae zijTQ`tJ10WC~aoXj*tLBx9q?J3~qt)wxnf~x{O0&VwAwOWGdfN_#8nAsd=1ZBiP)c zdTw#?qj5d{xqGEOMPfKEU(soCM1djIbw~?KsTQ51GyDM!Zzb-)` zs}=mY2*Y&+?j3rM&^+CB&+v!av{+Sxx+M4q3TDIv}H>F z`)j_#8k_M=+EMpP?CnfQZ0UO`BfU0z&7wB$S%|o)rnex^AsTe1Wr7B`QhHXVM=r|Y z6y-vERPtK;dx>lvSdx{@eksr>h|F!j;kD{_A=~5zE2zY~4k!~&td!|0n!?AvxUFcvH zc8~~mHkl`nj$Pp4z&Z2JYj;t#f75{7!0H+$$y6jZ4q^@9M;2b!Ct}a%8Fecg>N5qf zYgjMt#ehfio6n1FoS$|T!-JazQq@889c?~tY&=8_?w$0J&1}s?o6!GH(g0tEq^ht& z1buy4Zt|}nSjn)O?uzshDzEy1MLDAHzi`eY+y{_JCAF-sIrEM-(SyL+Tg2SFaDozT zi#*A!3sH7kPYA52zxMcOe`h7I1{i-Aj&fi8eNTt**_}j;S|EB~vrDI2mSh^>K_w9_ zvEW$ViCuYa>W3c@B$lD@9;6clnb3X;XXsCm0W;~^W~-~&85ZL%#>5H?Ga^P4{9Vi5 z!$MW5csHDY*egi1@15jrk#j~8>k-hOWArF!%M|c40AOpu)zmTausJ%YI|ALQm)0bc z-rj`}xcf8IACXRLnlv-O4jS?i#ejE*Pkvi{xAc>utkK|XN~)Gs%`S!8$EjLjuctU7 zvk03EMWM{Eqf_%^rbl?B#UEn*zl*%BRO5+1WzaU)@oMj4QcerU42#l-&w{K8(%ghF zTNPk09u-;4AQubEQ8@m7NlQq#?MmC#gzF1M!&mHJ$u{Tg1qlk!FgZ6Oj0XI~CeEr` zh?P=Vzwv7Gm%iCV=5&6C(*L_3a&g)&JCUb4QZmnW?(TyiFIZ zk{MfocpAyq(ypsYG@XUKJx2_=H~a9DQBZoFZlz8|jBxlgR^JNsm-OGMrQn^_dp|s0 zv-#xO&f{=iQSzb@v{Z%7b@pw{NK1KI1Hs*AnrWml62%Nku%q(f1?(nQqE_|KK}0FC zt!#*2HrrvMJ1gk;tb6lRb2I=HHhr*#MqYB5jt>T}Z8?9KR6%ZTMk$4_m1=1pZ6_Q! zS*jU(^tHqUx z!xNwk+EKoh$$vg{^_I1Mj=7y(3CDHZhZfQKwEOms74F+!<2w!C9aR(-D<|o25E}8W zIqnzfu0cLKFLW>#HC-bV_~wdW^QfD{N=c$Y1^qGksXV*|I=a?}H`_J(oNVzVc%J8Q|BbK1!0oJJrI1M>A>aHK z^msw{^zRw!T#V)E{N29Dx#yOw^ZmxSD-kPT*X;@wo$s;m5( z^6)e1%M4q2gD&7ZRxYvp#QjBVt4+n_jwMcMQHfi5(d%icYXsa2YqhO zj^$5rdtN>a*4_4OsuIP z76Poote3idY)VT{$xDCGXf=Df2u`2m?1_QR98uALj<@_jg=Cf3cAB*2<}dmCwzP3L zkLnKCeiF~051{3(wxR0Lu*;*!bF}?X{tVRu;mV5kS9a*|Eb*NKH@@j?9NRTV<6{Yo zftqKMJv|m)?tA|5S5<(vy<)cWfz|k^Fvj=X)nWpC+jgg_xY7J%(8Nm_2U?GxC=z9#MsBq#3f>265VE1DbNPvm{nN{j&^YFZXDT8Xvsky zg-0SVyPwGnd9o?bT@{&QbSO*el+O2Fc8*`)HV(GN`kor|s65&{@pW5>Lvtp`Z#%9R z5Q=ta2IOQ8fS6B*m!)1LeC1BiLYMCpFJD+&lk?4WPf03a69?9?nHVQn>QiQ6SQ}Fn z6N}4;P`YbSP7#=(>Ge%K{~%oJYd(AxF)p33=%#SrG%l|MZ+{QNP7(Rxv3M$lAVs(2 znqCgm8{D<-2dg7t7d59mWUQF}mo~bU3vKklbva(sZtA1-AFq6o7h68`efQ`s2bXyB4Cg5O>M5fm=ZTs5AkgyQP}C ziMd?Z;M2^2ShhJfI}YMsP5t5+>29O4B)Iyvh}N^r3$DqVHL-MI3q0ilxVKAsAO0K~ zRFOBV-UQv4TrrrSqYjvdA?RmHr3&DDA1Ue(s=Pn`1p$mF*#9#33zJo}r^lp$QpU&Z zn~)((cJSG}=W>((Y#P60tIHwtyqm+fH=|aLj3YulpA3Sv!HJ9IjDLHk9QV@6eskg> z>NlAuyz2gXK`s%V^NGsDVRnh*Y-BIcWGBlNzas!o`ho~3hv@cGuBc%oi~O1n4L15@ zO`sFz+kJ~A4tv)L;W-KF>TEr4lPI#IvV8=Z%ND{N)`Mkt-9mW5$hwxQy!x<# zWEJ-#p(F$t_-0>d4|QxX`|H9zVl80_Pu+e0*}d+JrNRC|sqhlgyxG2_troVkoe~3sSmVmU&*H6x3a_NG2kUQ_2N2uwkIIM{CEBD zOK5y@v8o*I##84mC!sT;257u_*X&Q$t}12uJWmDGUoJf8Ixm7!$Te`qpY14W|j%^{))byUFtx2Z%}e!q5?p~k@70}76y zKo2--5%bpvvl6O3b(ggWq#;(G&L=zWZiDUmv?m`KP~pJ|$%e~&frt$B*3%EH2Ad+z zki?byRxmMmW0|sk|NS$q{XR_fsQmChw3Qfn_Yw0|E~vSsWvo- z?L=v_qu@VfWGdz|`a^mPoq?;w9M1z>Kmk<}6`P7Tk099$UcGMvHfNp|{k^$QRW+wy6fHn(Hvre>}~r!{o- zr|zo}qBA1aZTZIX@mns3u;)a2tDU0t_xH2MG1$2!4!B1`mhHNyHN|Vz^h|SgL`CXC zHM65~*w$~yU{Bvab-Nh4S8H<7=VLCnAn(Vy2T|9jTz`$JmUr#*e7tkd;PX{4O>R=cVLt`|dW>VPSd#_+9|~nS z5AN&gyQl!=&{C+bA#ir%ehMX_Id5IxXZc=@%||WS#qUJqUMYKEkHhuYK1G7TRy;|R z5wT~DL}+}FEKWL8wtNHAuf=kxV}g8vxtCCV2i~NgEvkX1h&YZMfP{$-h^x5_U@8zy zSCjAt-b543!Wgpi)bjD?xmu&uE!YW&W(ZYeHGbE+5bN9qhXrS~v5WA?(|`- zeHDwD?_reLac+xPmO+6eizL)c5E@NF3Z<-YUypA;W;laQ1YrcZHYMta7u)&piaH{g zs*?3oPmSU60}pE1ECLSS>PCxRSm)n9UzT&sDZeJab9UpU5C11ysD%+L_7vjR&Lpaa zT^C1MMbmfpg)}Z6*oUyf$)MWpmoFbj^ejChi=#w%OhJR~+Q!K7&{KEl^7UIS>~xsB zzi8uNgdj36yxkpwZ>3?h`g7*PkiG7|>?{T?L|VQV?1zLXuPfq0q&|6-HI}I5*BZsi6cfEP89PYuFp*#f~^7S}btv&eVOLfldH zPe*AUim^TAagNz<&9kk$OvF^#{)sr;O3-{lGvmZsi0S@jG3M#@>fGthTq!D%DyC zGg*DdTnvKTicSVIUZ*Jv>wMGJFPLt{OkTjZ*m9)7WdAUySpuy2F%?A-7PMCNW4pe+ z?hGFU8!r-mI+6S{LB$n!!tvi)ad~`m9&aZ1I4rw|%q3B^>k6?tNAQtik@Js^5IN@+ z7D_^|oq!e%l)jPf$m_#E&rKNE#k~4o>AckY`S?{KX}XfDRZzfV?+hl=AV4T%;*tB1aVDs8Nn z8~Ht0g$#Gznm={WdD1_rdN=Uv9rt9MA{5~)@d!T&IFBKAuoAg*8zj^E5_>=G@9l9I zFlx8eyeM>_w}cK}_97a=LSk7y3tJWtAsitherZrxs_zYY~XHYsJ%X@XP!_QIk@)c9)baiMX%8^ zEo>wJCgR+09ruA2i!mEDlKaW2ywdRF?ZPg!spkr!IqLwjw(3v~WQ<9O&Fd#@Rr2yG z<@qQR*)$>b46<_)Kpvg%lG8WIo>a-q4SfX`mR*$;VL*+stN?Oc&T#8CP($>hoeqJpW_? zF;KrpHsM9Z9OA%x13yk^;ud_?%n6je)sATA?_ZFpzI?(y_?|dkPML;#T)J$Wq$An~m_?*eIP3ETWz|4Nx91^V^8ai>-bj zt|8VBQFmVH&ZP^3Uaigx`zbTt@g3O$#+<+V1ow?ne#h^yKZ2_4Uy;1x zcE)YhHt&hySp!K3k5Tc4A!(wH4Su^tJ#}ffuGm!ND&#z@rcH^iV;MEGyA7je#~vu) zZ>eaAU*+kgy*Z-UbhN6KZ2KoNHwFvX3^9B3Gifn%t5T7#s@UORPY-5T4Mj|>tH4W> zKwc#&7PgO2wGY0OQ82whCY{)*qXaTGJY1|#HieD^UV9u)?7W%`Nmr8DKbkBqPZ0*> zEhTp02Z(-Y#r6?a%r(QfqK@33c5(zvle{O7EU!vgtt7TCF!ouUo`P@QzfmwDY_au& zj34hor_Ed>kWFb>m{{CIy@nr}Og2;Rnpjwp$#{^+`CC@yD6)qd!9vIhsZZRPM1Bo0 z;rc~eLOq5&E=FMrZacv@x&}kR5O_`n+W!Ah^2p=x=dNU1ULZD*4{k+y#os!FzZ%u< zIt_gqoP7g6&^4Uv7+xN?2h(-@z|U-=NhfBPuqgDDkgR%($gbZmEJj^aS~Xh)J|0=U z+g%(*$k_zRQ<*ld<8clF^K`Zvz6(_nnG?LNzinY}ZEc%xQ^GrHTF01%2`g- zwX6_55G2FcPhX^iehJ9$+EU<|x51S~x4+q{uQBw}$i*&H^p{1RMt7PzTSBdx4^t9?vVYtK!Gq>WQ`&b)df8+y-pFw zVjIzNxopTG4(-~Srim;IujEN)g{X57?Grd)}&&yH5lAvr2WN zZk_)Gkqf`KfSK6D6^jZ`~VGp}7Hyaw$Yn5pMMwd6= z(i7T!|4r%RMP{DFOl%&SWDIjwrvls;Pr|K}jxPz6;Mk~EgC{-SebQir5QR?4kwOgN zLg{jIj!~YEuDzC6u2UnIl9MbahL$cawnuW{y|Jv)@%G(Pi}GsnqP8QQs0D%kG&EzO zAh*xyUfu_&*{IguWg2z6luMdJod$)YsYT;$P_+cP$vwm$I)EP3Sb53wJ70`lDO4Jw zTB0K6GhRs{NIz`prCv_JTP+FTBkZzoTXqbLutYtCtZhf%4rV8JAn5U5%jHu)bRv<# zEbqH9msJ!e2AO|;TB~}(zE=khl6nLsm(-RH_F3K#T(V2d{a9cx6 zsTj(d)kg6%Ba6w;4um2_T7vkeM{~l|zJ|O(+|816bg-RRblz*r5Kq-# zVBiH*3d5*WVuc3@^>*nXsrGAZ@W92&l8MjnS-4bZ-P})^&3EipSTE} zXX2Wjw|;AlEMh_|U>?Uzq;C2rS}=s~j3ewPqLA$~r_p`=_BX?(hB6jZ63{Y`tM-*1 z4vBU`a?|8=VDv*xP&wWLH7=Uj$!bCHwTO4>`&Kr_Udpi!9ocL>9?EDdJD3q;GRuIR z%?<540Bml4@e8V#%wdj*26-#RN<3TibSk$IAj-q>avlrX&Vv)&Kna^w7x#m(=Wcn#}rHG^yGsV-8q>dKjH8r=KE4SUsx?G(IBfJ%F^|G zT^hnrNLrc%g6atYA|qB)Ogwh=vp8P}cRHz1ZJo>>rh#Z^KD@4Mz)XrEg=HQTI>=k>Kh6rvsqUcHAhwG)EGz;#|obfX2y9RJL2Vy z$6z*J#=N-%Xjj5IE?FN^JqA7QDIT4q$>ui`+vL~CeuPxH`hF?zcq$+aTMr{Dl6v)M z_ki&{LvPqkc%~V2o;L&X>E{WmDqaHT_#Snp2JZoYhv??5tx}=H6n{P-2iBe)?&+$h zEX*-EF*t5%-Z(je9OMx9{lj=P9RY%Mv!S3=1FoK>l`yP;j@)4r5T!>gF#Y5}CM6U^>r_ z=_)wxjcxDR4kh~d`Oac*C}|^lzFw&eRr^%m|MgA(^TGkWe=B~K=W&JR+|5EfIdP`) zDsA1Y*~*!9P3vp$!mvs4nar%-@JV$0p)0XXuxoFXFY~5(%;H>CU;!58f9?u0V$NU; zgJ3h!7;`%;VUu!7d)uri5-p8idKBKBzn&^_<)!(AOo~g;^SiZ5h)2EF6N4uj@2iHJ zCEnM~?&3V$JD;M4!Vqp0WlNdIOOvVQU$l$qkx7}UlNG8&XBub$9y`v z|Bq=B=48Cw)qf|CgzYU^hfrR*mXS!{IoKWagqFI)Z0pV)4fqlPe9A+ z#7ttYi}1}rOW5ZkM?aWmNURu`5yJj;;0isyeZdYc7J?;4-7RweFf^2a+2Gb zQJYS#K(T(r?epo{cGxMzi`@E7*g?Z@Yi}W46Ybop-Y?rjwDMHZc=VX?x0IPYxe&2+ zrbWcZBXCkK$mS3>@5kC?n`4CA#Tg0>tb>3F$v znklW(J`uGP(Wa=bGe31rdu9b>Lfzrw%oteDHmKV~lQ-XxyqO(!RlVJ$9^?RAh*PgN zLxH_-X#dDo|Gsz8&1xr+%bJr(CHQ*jr6sU7(`tYX#Jz{~n6^$#(sHN8XB*vGo9m}5Y-tW*dt>A$r z{jYq~OLhLjh{)OS^H&$4D!;pa=g`u_T7RXru9`6eJ~%{p3j%uT2HEcY>@w6LqrLI7 zRuZ6PkHM(^-6@ZJ_U-RUKS4ZoH|X^oi2CO$Ot}q9 zHg{Ru@{OS-NZ#6-LY9wq7UX=l?Sc|5bx{^mb$&;2P0e2%WJy#(xo!BWs0j)DzBv^q z5CEY|w;VttkyVUmr>SbDKZ{iVZUm446Mq<+tA6zPm3o|D_Z3%ZI#>0h>hDg$(u z&Z$88JQ&E((s0#xcVDacWVOuJ?g(>+E)X8bwX8ciW95w5UiUdo0X%e$@7qwc^W^@o z_9`jt)zR#EMeig~`w!I<&HU%lkre3XA*);_FTBX+6@-hl?VJR93fkY+%%U5ruplPy zES6#`D4HH8HKbCo`|os7uH^(1rw>wvD!LZdxhi_vUVZb}TxuZB=@5Emgw_c7io6U~ zbaY8(eCG^u-Q<=mkx?!2wTHUizDloM9h?W!PT<(&){<&c{#|3U2;ALR%Abs8gAk7@ zf||gdN16Mt>uQWBnf3esJeThbU8Y|j&ST{jO=N4GW%K8v5?!Kzvs^4*?5}3s!On`5 zvGIFPRqz}JM?4#B?ZecVVE@cdhW5?eZ$KeR?hW^E5wH!9vgB#T7n{p`K8)F^i9Fh_ z8x7p$E#A?@ye+>eoWUy$#B5bofqF$!Na)+vJ|$WtIp3&AVHJW98ter@m+ztAmiD}B zpx43Y4ieo4!>0wa0k^Wy&6f|y zEP7|*5*mLpxeE9SmdevDJlbF@`Rlx|m5WY>P{uG9jy`Js>NM7k5bF5XpK`^}S zN=6S(_vq>3FXOZcDYjzRJu0mKLwx0`<~>pGi_kO;K!ftWJZ~(NfV<+ZPL3GLOD4br`C;>eGT@Ax>zzv@TIk%{IH)#*|FL7_<_nQ(% zB|OZpPVnS^YDZ)1MHLGd7A1#U@AKpwwDaQgbI904zqL%=m8xX8g8=|62WS;ArB^%_x{?+MR~vc)108K=OpcPa8H-74 z_VG0z?elZ9!zA{%LRXlGUc&7Zc$BC8B0ft?c2m}`*g^e2tvm*ZOspbXRNA(ix6L8# z_07iZ`Xh<9v}4SsZxqSDvKyV&Ilj@NlxfbZis<834lN}7z2#ZnFZ)AF7 zvYfJSz~96hr0b+{i9;!>-^r#f1Vys`amyNSpNa72A^zUZ2R{ZvNr_Dv7NN*&|H_}f zSsk;G7m{v^*_)=*#qQ2y^qHMy^Pys_&u5i`cj5|KPoZ;u!H15A3%%>bC)EQR+x1RY zmEdFXfHyA+xDt96r5ZD7x*@9(5{j>Q6-L$T9UMESOA-}lw>mH1xSoFx%qFuL&XQMl zRCstC2siZ^&K6C5KPTx3Sz&8eSsVEB6%(mQfn`8bzQ~329t;g!kvJak49AQ>?%89! zjP?%jd|am?oiSPFZM-dOMr9o!gm>d5wPbE3L*z2J(ul=%m$+DUL-$I{*hIfA49w89gE3KO`I9ZWiF1=DBuQP zw?~U?FXaEZ7`~#QTe!<<184;|awOt5JXfya^c)7}Kp$s&^7AOo={iNV!T4m;*@!h^ z+AUafoR3_JHrtXDqKh+#!8tX|*81*k!Lh1TAI;XK!&~_^bG*!hM>HfU1F@m|-yaqn z`2}DdCq;q(;$ZN9La<4)$8~M#>b3?I3MkgDT}MYUeN~?C`u3%L2jg|V;|NwMzxcvu%5j9;P3<<_u=vFFa3Fc}^f8xyOhI^2An_o|wj%nWo1;A1m?$C1b7!F|jkiGS( zXlI0>io&C{yBZkC5~OD=1;TyVcl1^4Xx?|=YC#of8bYVHpZ43M;*0{cac1ubbl#5AQNw6Rx^_l*dv+iOM6d&(+TSwl7-$jP#|CI(MPIRD*@K$3&mG zRH$H{`QANUR?zcm3}r>0FUrF_qfglVswzEts(Wxf14U zjY`9YyxYDE`M7NRr^ozt(&6y+DCSa$8bq1)>)i*lSuCQ60Jy17I&$yBJ=UqnRK^!# zQ#!h-_iVqnQEgr3it)eVAs$+`ymU6#&0x+)lWf^HE*QG~LdQ zPo^&AFUo0ANkLqW;|R{3OZbOUvk!mn?Cf4c(pV9=7q*=zhJ-5h7P1QA2vA^n9x8R2 zwIXz~iBMPI1X}*&dv)!Ov;eQ)Iyu6I`SE#R*Rb2!;{6~Rt}vn*G+Ko+Uv_2VWhye9 zjHlxg^8V<>)T3m=QDMzpHRuoW9vtnNrjM>q_0V0Yz&XGNq9h$F*z16C^VStDPj7Z> znbKBGX4O1|*AFg%FGg4{@;ESMLP}M-t4LbWQb3SYts3+}FPLaySB#!g(UR^C>LKYgQCn?K6E(3J1qSDU1+ zC&(_G<$)5+Rw)u+_Xj39u|#f6VEjtT)}ksbQ zx_p`D{i`}3#$fz+kLw+oP@dMrt){utL`Hec^QnLSv;h)!rc@2#E8C4>8f96%n(<@u zT2bZ1z<}1ARUSg7H62*13lo=ZG3W{seH3<#U`F?d>2$MxWy>RdU2j|8V)#_->D6&c zIS=r-w}IC9>PXkcsxmz6>R--C$PzcL4j-uxEyj;b0jrKW+Ui^nC@%mWUtSHIJ?`AT zDP>X4$PpNKH$kUzQTL)!$8{g~v*`5_*@PSXG+b%@J~+eIW)N-V6PWM3h3*5w#~W=^ zl))^~z!=qQ#alrEx?N0Z!AT{dt^A|+jH5j4AK%hD;;n`$*89I3U(vgGH1?)Uu`@Ou zxZ@sPR5(=Hv{v=ULqsh}YgirMUvp<$Ps*K98)d#w@j1+EY0(@@<4Iy!z?cm_wk_i( z=jvB|3^2|5vC#Gqk;?5_vF}Xw-xP4Ctak~cV(?)38>*nK80=a>zH6-MFMy7pI;|A) zDDgW-j}=NhLJrnIo6((&EMNJlO>Dc~&3B_1SL7AhuJQ{msep<0`(F?5+i+ipe-Z!r zi7B~W^dQNe<3my5Dh;wI#Q)_ZLI9X(yRYTn-%A+nlNxM5^)-9W^1ayZW>%{Hc}|(Wez`e3Nd#d==`{(L9p!ufHPF0;k0yhK z*k~tSl#Nba)Xtsl;vt8;(EHKS37r&!r5BkZDTy5#m-e4_yXfHJ|yqCSEd=*j(ykB_nA=PsNX-R~obiUj_7{P;)KaY)i{DxbvwY-;seKUZ}Dfoioc znyyiDHMv#HC`RYjp(Q;xX0Pw;2#j5B5u7L86K2U3FU5FiX^#3CqgpTrZ%&+%Zax$% z0Sk$@zH@(ZLz)IAWZ8>60uywv1`dy!TMiig>RuyyLM5Y{?@dPSkR64(KC*1YYPk(E z)m<$f0k>6xif%x*xvAVzvF|8Z005L~)?tYl-~T-H$zCW@R~+1p_QEeIB0 z;y?RlUPWM2W9G5H$68>tA>k!4er5tx7*C=ha??XuEw1a&J~j_SFk=0ZBo7?pY#v3* z{ankbcxB_sZ#dpk!QlUXQfE?3XQ=OyB}~S!Ah|=;S0iaeBiwp7jY;>H+&NH80hhXY zZqc`kD(f5u{ELOxUh6>ciAwHH}ILb;+2j1Y@gzb;C|9VHEa*^Xi$Z85UeFA z4c0Y8zJ&s+f7dpoZi|cNM(`i7O^k|ryM^O*O-bm?fJv-LI35lTf3$mBR3&G( zjsv5Cnl7)Q;@YC*jm5*EmIaNiKKj`KLXO2mWrsx z)bM+DNHK5vD-0MX9hc}v@kss^Yl@`v`1I_GY{E?TQA1fy+zMlc(^bXn#e>}AAEV6(-0HzG5 zRq$|k`iqwHVZ|z{4*hvnW2B;VUfTxWL%}Bf5*lmzl565T zgK==36$LsLWw(4XzbvAWDshd{mMRy&2l9BltGm9awg}J3Gc$caHgSD;&%s!-P|zlZ zlc95QKao=EJ53>n^uq$2ufTvxIFVyiHai16vZ?JRQVN8Qo!#TK-^n?@M!J@`U(=`q zxDLOD+F_>lT1rRwHhTXHVTfs=On>X+!buI1=r=ctTOc;4jf!$XOmyztC*7l*j|9l` zB3N4qsU`(D{IZ1xSr~fv+R(uoaD#TY)#h&a5Q5)NJvl*VIM1{3c!-0;uvKOE8DB*h$k++)F_ebb) z<-CrIA(u-px&^2AH=CU}HVe0fuX9{_RZZ9vc|hPOtVK#D?X$qvbdsu0*2_en9=-&%XW_G9(LmoTRHQr%-gFhain8vTQE&3OS~WRwjs^QWP@R@`m{PyppY zyw@s*&L@k;bC3z4m4Hw%Wn7J&V)79{|{ad0%C+iq4G$3oH^ znV>rwF8x!PKo`|HSxL;3{JCz#d5WcO#OuV))pIUdA}T?yoQ4-)SfkL}5Q;!%@qHe9 zZO&tOA}OWftHxX>wL|-D#xuZr` z{FQ&7Hd7%u$rn=K^=!wO2GC^>1L!*ntB|PoGQ{ z_7)qnLDa!*yzb)8OV`M>m~hUStQ3j3gt5iAZN*s*LMO~vlk{LhF*EqH>a9eHAuqOR zx3kUP!2qFg?kNX}`8Y$9r@vN;V}mS}4Rc@=1x7XL_Z^vG5Jg@?dCYO_S#>2r3c5Ya z5N58vYbJkAkYR(ZYZO3IHjbKUb-R7`rnFnyNYEx3d908yrHD;$vRG7SAyK$ed>w2< zh9wN*54V$E4w1Pu_X(t{14z94@0rkVyKqx`{wqtOnCf^7;1*JYH`X8=8`R^RvgWhT zI(p&+ng;7KZ?7XSViftKAssECafEP9yPUuzR`5U>xcGTk|KV)iTGApJKP>zE(MYv zcz6M|0yVqaW(FJLd!qw$DM}ejWhNx~m1TOgQ#GG19FRg%-#LAH<+;@^IrHAC44Mhy zKz-tiR|CMR%t)d9M0;s3T>F*hH$s21&Kq1m$}>K1QA4D~j_Isq$(MG~8Gr@^3#d~*?p zB&R2e?jsfRV={6o#|rEAU6LZxOYAlJ7e%s);P1aWFkIvqu{`vg*1Y?z{7t{ zLa@X@2H%BCd8g*R!PBZcL<#4VtV6Hs35IfybzftfukTIgMruskv_lILTw&qhNc4}~ zFrHW(#y(L^!Pdrr32a&~;4nE?^+L%rRG!1h6@Scc)w=B9cOnwLgi^i=W(V7W)vngr z?LDcv>^{FqMS6>HWwAq|HOZT3)zJ?}tm!@OU-(u=DB_ZiYh4bewmk7pkFr8yDDH@D z{2 zK@sy&wB|^9E4w^a`CQE-3~M5ufX$& zvqiqbZ3G?5jW@25z7HVBxg_9>faDUVgF*z7w{XC*fKC<};Y<%WKJ!~vqYjALs+8$# zUx6SQ5Szqqv;Zn0jwc*5tKpi3`g9itXN0=d$eUNudz9Jp2Vn$n>$gZcfDcy0qfWrv zAueyrw=N&_eu3D3_&A_3Q<7KxTrD=gU+{|o*+ohYnIvj1c=r2h#~1(Ko@`u3>)qcE z+=9P?sEwGrrD!5`!GBRPdG(bhN+2U`N#1|uT%#&6ar7N52s*2l``nI+B<7nk7)TnD ziIiyTqhh1qY+XU{eNF<{SWAi2ZKi{7DYzbdTDL;uJ|T6Ua)&dvWG}kMtss)mE>}t_ z^=-`}5|9u7yl15NiAUvwm_`x9?`JY+`LF&Hqb2(@3f7;DdEYqanP2W_pBPJs z89^)EM+uih0s3vYNqQta@*O&AcF*QDh;5orA|96wYv4pl-$EO=oe!m%j_F8#=>1b8 zK6!2>!=flZ?C;(6VHAf$Uu?$>oHu_WJ~JQ4@I}3(yv=kBr|7n#t-+?=Svmz;@X(PK zTl~!XHmZ>B41qB$3YZX+&e~PvvB3|>(Z%ovfNST|CCInZokynr`lu0S`}ngwLG=^g z|IsV?a^|!_H3zLB>nz~>ztuVjjEkQtb@RylycQ$%_cw!auTBQxUOhp=p@Dl?wf(z= z3!_fvdeSIb&fXmTaG}Q6Tzz;2%)BX(3gS7=MnoPqHvj303q?a-W|%;n^+_spsaT{C zp$q}>0Jfj%;N2YGvCTo#%>J12d7ELtX~I+aGMkUE^)R0&_w$hnETE)EqpY{tDQYp9 zEqcutuX{)Qvu5z4FWXCzvZ`teP(UX-f9)80IXilIbj=7*jFrhEGIwtNkHO(p)~kjw zI6V`v)Y5NPhZI!8@)#p00O7`ad;4`x$xZA^@j&FtFIcM$b*_12R!4~qQr6_iR7kuO zm5daD{9g&bo%YCyma4$Rl}^~!1{%-SYzEfok1NEpY&9mA{$81cL-3Y*4#fOxl}ld_ zWb8%>4ku!|O;m=U{j3#*Tczqo7+a1_;evfR&ez^Iog8K-Q4jH=SrvP?U3h!sCjW=3 zv+#q79d$K=pns}8d`J|ZwSTg_?zkx$YvpvlrG-Lv zj<^*W=21_O2ePyF{{ArmmOGuqLWzbzVrA&?QQ`ITL&{o4k#FBxF~=w_%gBo3dlgpx z_DxXaBJcA38ZXr?RRa5^OT{7lI=x}S6H}C&y(0+xxxcf+L*6w{eb9l43>kyKy@TXZ zz6#a}bgkRZ)0HBJBs%orib+38%#S{s7F|@NL+&mM)cpR${P6J~lTG^XS?V0k+MrVc~{dV=9Uwgf3Atom7An8rb1 zf_5-3Hc;7=bsHaF^IgI+dUr1XGk$kqgP_E^@Q|rJY^Jg$v*?R~1)=~#G zbAD}6_=paEmd_i+@L0z;1g`B(8%$QPx*?EXtSTV+d-2?RGliGyJEOg_8KDHya zJ(3a42F3n+O_6p0&DWUw(?x(5hzl~QGmQYO$w}0%Qww0LP9DQ3na&UWlWD}&tYvvN zq>{<-LgFQ+iw&wTK*9)kAEQ#dY@I?E59O2fb76Tf{E?5!I3&h{17_qb4_0LM) zTgdPdYe4@K)v`+itY85IW$tGR7gk~ba1}AbTvp}zoEVpUtlW@9=PZfmGM@w`4mWn=BQ=@my$5Y z%E%5{brp(k%Iw*AfU+}xc*g+unSD?B$9~7CF(oAkIA0^gVxI348uxYAMv{^^%K^h{ zU)^7#2tob+c<_kl#-@YxJl``8*z_?TW1Og7Q0;vXsqKsftcA@d zc^@MPKZ2A(5#&nhNy%b`qwcW4*X&znJlhGMB!6AXy7&Q5wWQF87t#Hp1AWq5FUJi} zA(}U+mhJRA`9=BBqOiZ%M3~4qoCoq(vrhK48sDFxNSEGM-RS)GlGeSH_eY|mbEu`+ zFlZoFQjm~9iJCKD(wkgIbX3W06FG?mX{a!ZMOWAlO?#Qs!w#!-*EIh%x5qeJqC=|& z`+fn;`6-E&aE4QVL%PF$A#a{D5l}+y3oVdSZ>N?^BFOCoTBS(_JRHX9FSG=SB+MoN z^Uju5rbc~&HCqk+d*iU$BR~x36Wr{~8ue=X&^+v>5q77pZ$>+tc6v+d@~IC>f1;C1 zBR*)|hQs)D<2+mAw+!R}m!43X>HgO7GKBOQ_Zn8Imu*s)(6X!aiFC%zJSV(2Az|sM z*Z_9D0w>;YrLAvi!+>Tg#xrsJB?Lcf>O27EHLNEMF!&?vrDkyoTgM%nD#o}S;%%bp`kTZa3ayl?W&yGs25UJwMY;D#OXJ`tlW>zU{4gXcA%CdaSL#^ zUDovXf2tuzoffhLQ4bD*mbU%B4T`}S!C^#P*vVjSgrz1}7W(Ds^al}k`S1{x3mRv9 zwa}-#tr!44p4f_+e@CUgS^SBbVmw29A#NK!(Va#xUXsEi3OBM+bLTP-^wKEd*;^O^&+m|^>$&;<$#A~E^k*2Z|A|I&C_0U^ zebPD68n*EJ@#dZ9SD!97ma5|hkDf{k46A23ld<_%7?An1S-8D>VC?DDJIwMHfUDz; z?-}eWv5Dz>FpS2(^W?@0(6B^M5>PFkU-F!ZufX4?F(v)yo7yLQ#89m>JJ1wlCHj9{ z$J7fs&yw0k#aGaSR33xIkQ5a7n>#s7Y?(pSEE1*tV1q6k>sHgHfh_lnEwV^E_1^#? z`Q4WtxXHwURW35;Io7Z^B8P&Urk5Vglg%;7zn3r)AVb6>C6mMG;DZWNk9%YN3c@86 z^QXKA_GEE-+*kn(D}lvURHYT=clJny^lW$g1z|Y&d!&Fti6lnT6u1RO>SSC(=?;=C z0fHskU?kNYe=#FWu>un<3BH6ox5`Q^uc+40Lsbk%yRNd$f3EzHwvmK$4j1HM;M%X? zwe8{9gyiwMeg?1QK$N%BoB0yl!d2pSIpalL`p+?JU-`Cv>{eFcWb755RVFhKDTghT zV9RXrQve3wSeUdwtAtUDV#xU%*uXONfhg|-?sRTf|70xm)vZ@Ei+Pkd14TEvTr?Cl zKD+#0*n#c!%==x%o2uzMmCr{3DPDt}FBbu@LD706#V+~v&9h98TvJ{yhkBK^E_GOX zFr#VJJzE<<#zIKw??w{eKZn2PD@)~4`Ez9XmWB~5Ng(Afslv?QE$NGYRB_f-nNBU8 z(|)^qf0YNE8GFY4LCnsgA%<(^UHKa&S4i^Y(dy%gvBOUnHA?k$+?NjGQ`vpwe%5Nl zylI_|6ELPQ-Yt{cI$Xo#3R-M*jv#~V?vpY)=r);obKRm?6)O%PdYg(Iy@a@n@MHQr zoPkId`(HG5PP3=&h8K$K z-JeQBx)IX^=Ux4CR_f%h-0O!&udrNJdl?V#4@O~as#xKC`>!uB5cL>suh7tB#l_RX z?0BJ(^%d0GbeU1%1@|c6my}Nnd)S_oraqhE?hk|r5>*cI{fJqA&yIbfo_H*?ZhE`D zgvH2Vg1b*8>yK!w69G*g5M7GeUq4>iwxF1Yl?_fWRoU~s2?8Prj{1?%B5cXZ#$uAQ z>V)zwZyl2d$SdZY@OOYiM&cJk{BNV14Io6jLaW>2@S#W_6uVubgb^(DUpBhEpD+^F z1Y`QUMLck0@M&x+?B%4GlEEyqj9sl;r-}&7?=9cbP%=<4r$>a!R+LQHqzR%}OeivB zRMa_1P+Hd7c1sy$p2Sj++J&H7S7!OM>Ewm-IVrw@1aAYHGt52Q_DNCE%iBIIsya%p>79&)ENuMX?<>BGqO?p0&p_C!Kgbaq>{B4Y6%)V}R>+c7Erzi74Ux&9H0 z)MraLIYGIQ_oqP7ol_AY#^Q{oU*ge5!c|wk(&Qf#O1h9iAKA1VI_gAXJXJxH66HLf z?FwQELFcb45<4+$Ck5)W2@-S;xQb6KShy1c-Y*MqiYJo7Z4|Fi^R z&R?h*XTNZlik{JY|1vJfHKd;R(P%gWholqzJGWR)jS_5sQgw6YhZ^+5MG*FOrT$@6 z30J|}4L1dq-6c$KY*4%kY>whq+8e+`5}0AEfl)=Wta}`!7XN!GWxnBzv|kEx-aGFS z?-Lw05t(wyHSDKad5JJ(mH zxE=eptdcY6Ii>zm9%Z(_OvfHxiaVwYvv0U;hqOi9F$>OHrG`gXjCa9{Mu8X~eR3`9 z)?q{NHDl;vG-?XeLs3whEVJPoMZW_cd2|V6urb=;-ugAkwz-V|@YQ>JMZe0;tTBl= zsbBdPYf73O_Qg*IXgbYq@3x0zj=i7#&>SEj4h@gS*1N0jNVbD*u+vQOcP<+itHId} z7|hja8o=D|lj^n`M+Jv7;K&lS9?4ev>dviPcHxRQ1+V0E64+`Rhn+lxA0A);LR7<9IrRQ}onl|SMx3|7CmR4BsB?P5-6Efxb(nh})`0M@KIZRj zj)>@(!Gd-$Ap2{#LHN%VH|uIBY$5V~z?-T!8F#DhJYUQfUw+PI;h&}u+lml75qm`e z>x@Q{+X#9C2wAmvaTEF&#z{#bSs~5GPmvU?cgA#s0w-hP-tElnZ}w0oB;jr8)`gIc zRW}7F91ytS@woB1=jdn?J_oyg=jg^_wOxp!pzCceH{Tzekz`TY;dI;gUZwc@N{ivs zxWlcV;ovMx`$Ue6o^)0Gaurrz!!nk7Xx6PI}@V4Bo8Nikwo@M zI0TQyX1eAtMPYrKw&kHV&#Nq1FcZ5;mZ@b@d5(0`cnX-)7catNhb^nzdy%TE()VK3 z@82Kr4z0JzIwO)y+NfGDqwfqOfXHB4>`zJ^9fPNG| zlda+4;pM@I`|%0_%7ar7=woj7F&G^Y))B*1)sL)l@nC*a5h^VixMPJQE53e^mhZQt zcOyFPvE{_p)cpzIwFoAJSI2_4YDa3LJIAUSkHNY`9Wu`!02R>}*0V6|%qbAJ%Fo1F zbzl4MjVgS551`b-mQqI6@NCXbP4zWK+(SG0md2bK&{Mt`FcJ;yGb%VEmE1fdE%0{E z$3SEqg1`0hf#K+&1gv$ahk3Xmf=R#1P8FBY!$J(dmxtM83VqePfB%5}+{+EYHBBcz zWp;4F(-T4-_z8R_0zkS{>+RZOy)A24-7qB+akmDI#6x(t z@qcNlHls?jzZ~HX;O2SaNZ8MB*@svb8~Q>Y)fIOPHK^ndhYw9`hsn%ssVZT}O<$Wq zUvbbwn{~BYkT_@V2CR;xQzi@*nDH~f-)MvCcs26;j*eE@^8S+WsJFRRJ%IY$#+xHK z1UL$#zuz(h@T@1lUGsp3Ly6ll{8O5ezSZMR22rQr&4`|aG0eZqxqB%O|Kh==0cf_Y z_w|?+^CP-K4i=>rI+35>xrcHZ>uaqKZHlV|45&nGS0^Z=OlV;eW?Hn5x7xgWh5#y+5qO?0JK(0bg8}P(!mb?(0&ZF6-Tmn&1T6 zLS#Ad&2%Pc{-~@{09!n?iF9}Ff?a<4HGSFzLl2&`iZbdKK3uoPLMPv}Lh9AuJyGoi z7^gC0z}uY=o=f7h*yzz2aSp~k{dRXd&-z?RMiwW(IHkUu^QUT?iG(b0QU&E3fmXgX z@8pVAU$O^XCQK~j86?ru`at@{j5L1RUyQ8k%-VqE9foW0R7v2DIeeoID}@$eXQuc{ zAp!Dd(pM>EsVD##JOm?Qm#33U2yi)sJYJ^)7htEAWGC{L`u^bI8XN&_Cis~MOb}a- zR*rT$HV;e{N~Rcw0B%Xyq8Sp8_s+1B9OE^(mhc#V|6NRg=>ut=Q2_GdPgT!=$L+Ke z7&oZ@={xxZ17Bh#*&y!WHjhFGn|7x}60?nAwNj|iTjqYyeJ`Ck9E%+cnB<$szGq10 z88?NI6(H*0Z__NHH0A}%g~7&&XSGEWj`4rE9-k)aa@fU!&vs~}Z*SamQm5GW&E6#^ zc7WaNp09HHoY!ugrq&B#I9xW|e zOei9nUbr*M?hW%||Gr|Cq+$CBdEpJ?=M-0(6oZxj`2rBHa))08GG z>crdq>i*uXKIUF*8h!k6O+{UQX9_a8Ait9UwgKD9CkMXG%tm$3@s0Pfs|AA*QrgD? z5z}0^DR7b!_SZc6kGITYnDYU~#QvK<#PGTIKT0snHJOEt7{K*U9j8t;JpkXv7S>f? zGa@%mG{1zxyKpDg-z-hV%l29Y0dZJRkQ6Oj3Q84<8217Kqy_8W^PW_f;SNF``TnYj za-RjejS;_d54t{@+pv8c?4mvC%6mHLfHVu523Xllh20K%zh*UifUTP>fCG|kThzNKoYVKXg9V;1j@}kx>1YsbrI<(d zURO8wgKY0=RWCD)XyH(z5hzC!0)@EUm+x=IXxlX7x-Q9a<_?|i)sDKIYQEgc!R^xJ zsjUR7FyqrIWpSP1YL)zK^Aguy5}pbQh76tYnq$jw_^D_`wR^5${J{@wBU#xs9rK$RHFS zdPzHU#Q$l=Rb0{L^d@f!fhBT!*}NH~0&dZa-KF{rmSC8yFH%X!J3I z9Jk>Yg!mE^L?lcm})x>DA>%BA2mxZ8ptyoKBsy zU2$uI%!BS6b>D4-TM&Hab_KPZ*dxc9uVG6u^+J`Cn@<- zQiWZ@O9=p__c>o9=xpaDxPX|>O4$KRl0kF6NXF5tx)-%jm5`SHV8DS!?%!QQ=pW)Ycu`oRJvt@}~)CdVU*!Ivh)*vPo^6lz)EOP&E@m0@BD6go+ko zCxy@=?2CNvLO>IyFOFdu)jf_g)21FSQGD?Uzg-sxRn$*-%)*8I9gn8n!XGQ@)tRHr zcpH6JQ&Mmmc$KTKH@(8@dAfcs;dREc&Qe!Q;;AA2`I(ivKz;PJbgy71@a_gsTJZG? zv&Z`@K-Z28B)P6@>krviUKb(B&C57R_sBYt!9n-GZR<2acaZg;H;EY59u*&0F6!u| zq(6>=KmDX>WZDl$ZvQ;#6sKS|yx|sj58l^Re-7~4KT%LIFvo^$tvzO}7~glOBU>y4 zXl?TbgZaj?C+8_!9)uvVH=pwYGHL|QzO#CruYPoj4N3z$wL=urxQ>+|h^4OuF;c|i z5F_(^*3hV_DbyPxEx4TT?B+7`G*8BHQ=+`LJ2RVc3tmkdI^7|m!mEclD)td$XXrX& zpO14)Y@@&WV9ydk4x_;GcuS8qJI|7o4&9O@nq zTe~r!EHRB{{ zhM=J8IIi37ifCn~P$`AxoC|DclL~~LkdW!MLWs6WC*&uS3UiiKT*MQkC^XaBY@%0H z?Agk`Ilc|x`P~g4o@w+ZlOFo8(@9caXe~JkQY8Ex;4l8Mk04Wy)bz*kB)kFI@U|3O z4cCNn=K+b#K|2q_O&Yrd7m2OZ>Id3jA~!aR&8j@wZ%6}3TJmgd6QI6i%woeR+4S}h zJW^gLcMmkHReT?(FoLmeHO>RcDqb-faXPKVIJOLAOEDHGOFS%yybZ6N=j1!sc7&}Q zxg4$ng20HSfhz$|qquWMsf~jN=AIH_zOMx9V6|5(ui-!iR}VM;feC<1klXQNKXeue zf>o6$-y-f*9|FfcUh?+&Hh|NHMuB~qQiq{bj$tEM@slR4RX-33el;b#(^cYD%i z5z8?m-Cv)Q*FFYTG|KI9Hl?@cxMkP6vNvkz@FER9X90Tt>BwbHZRt1#f6CZ6KB9UJ~ za}?|HzTeveN>%7=`gR^rjv=oW35F_8QGi1=anEE`KIJozq<6vN%h$Ev#%Fz%df!v3~IQTVX5k^sDx<-OyML)LPQs*;49S|&YNatq;NNv0}DS`iD8>3 zeK$N4l-G=?B*Ys3MkXJ&a!b`lx|H)_5@VHMla!`jX^dzI9eRj^hs06AWN%(Oq`_StGmR%*n^R1vezx9&?Fa#H|$#LuS<+dtuc5V7~ z0~%>RMhBh~r3X?>5-8yIa5;aW_Net+WSeq(oa!?=OO6aG8oqcb(4TjzI;~~7qp}=- zVPkm-f2`C7Ae}`2c*QIUx*S<*HW7`^Xo}3d=a>Ka{s2kVG)bO)WP^Q9O1)+jq>#0= zBnqHmN_zLJ?I8Y2WUtDEsq?}pt?tO14jhEOtvcl`$R@V4p7=NC+>_>I)o=YP$WhBu zx7TvW_rzaLbYom1b{&%jVt=j~DWT^w8Tc@8@e=zu{`b`YZTZ2Rs=?BySY7XZghT%@ zeYXHT#7qr5yy%PviV0l2;lr;2>E zmuyj~nW>h))5LA@#|DjqxeXg_v%t$C$6;6C<#D!!KZtWUD)S*7hduXmlagW^hufIw zK70IC9Ee_@(haXap{-ZhvWY*(ZDkx#_%<(G&Sa;gwax(HVX=PU(2mQ5c5E8=Bsh?K z>-znaxHR=wBns(fRN}H>e+Jd&LMCs=FClW-F7U{sG4J5t(jv%u3e3~iq4hGSGn_CQ zeARoLl1k0%-!-d18@N-wg3F6{I2Xi^!oUKLAXeST-CB31A+p8Y{<#)_eoJM~XBABw zOJ6e6Mk@X4KfqWTs3h#EAv~2oIf?TdCi&J5RW{r zr)2%O!F$3#?goXN6j)Um+4DW{4(yz^}lj1E;MqT|(AH(G460 zjOlzrjyM?cS^s3NJq4UeHHSnk+a!6^uy6g(|CS4=K!-s0QL~U8^)y&;Ja|I>ut1$P z@hKw6R2}KtTl~Ybo_krq1#zv9{JN0E$sYE*<9$t73nj3E7@|Qx!AeIoDS=L-%|#qY~L0?SRFHmH}ivL*0XI=lR7F50=q9hNoN)*23vzq zO7mNdW5L!uGB!N;iO#U#pCpn&*T^N?u93(zf07k+2Iuflgf-n~Z6?IHM6=Ba6I!Bn zj_%{7(pt5mocW`Ju_#VNR{)UpwDaJ)sE}m#xh2O%PM;wr!jX5{72tF zYKGokcd&_>y5EvZ` zvcJCihL5WLU9M8xht~S%d|GHoJYAwwW^9!v?yEn#nRHYPR+e+o>oG8NR(d;{XI|DV z@e3-~_acV;olkvu`Ej{J{Q_{tRJ(2w9|_ai#py576M;JGg;rNgQ((rqVFClG2KM0J zE5^?mDszv^T~~xea`X;bqU#jocCJ8#wl(I^H2}-H#zE9EsG_ags-b1WVeR?RUPAO|H)OQbIJdp{FjEYjE&_uj95{EP&~_*2xa= zT$d^5)r$gC~wAW!?Dp5wjo%G2D%B?c>d#mLepK0L!fs{V%4f`~lpZ=Oa3N zXWf4g>k%J%?qU79uB7wc?RlSMI~^R&xZt2VO_US7TSxQ+!w+11szgeInR zDIYE^m>m$KF-bD;RaF|5;_zckAi-euVRB$!q@H=ACv?hIU&p7P+%=8&m2?z8_>)wRVciLY_cSNybJ0yxTd?N#TVKoo4h zM=fx9eVXXXH|aJFik``Vef~6N`XTNw=;aH5~s&5z6@Y1 zczy7n28&o9sK%hHy=H6J-b2j~4M}v&V#d^~{Xa&sV|)LsLi^nF6jE^Q18+T??Wp*@ zZ-9H(mHXY*xKz<)th`Zni=x+xR9{ah1$UVi^=EN5_3S;Mq&t2}^0;r5Gx*Nz$-K^s z@1@7PV-9b4tiP&i{eM^h?ar=Lp`9VI{;Un|wB6eL1`+!^;dE;3Za=tl6$j?tu8=On z=5N%XT5g+q_8sqy@BLg$1T_d~_#rF^hx7-5aUfJ?I-js>UkiP+4GWqqvXA^s<#Kw= z%NgdB?X70Dgx|jFGf`f&MN@89kDKxwDhhQ5&cyR+DR_suTFkKB+Xi5B$osSU#}E~BrmxQyxw6ko$ldm1cf|qQyc<|ynjg$?x%lk zTX);?Y*KvRG=w`yxk49Oa8ogG?q%A%MLK0@@f5h;8j!K}!6|vG*(#$gXz!gMAME7l z7;*D~@|}w^@fnO+_s|Bue7)Q`2x=7A4*_Fp_3J+43uHUHT^5vHqz7>YH@XDnM|Jlk zrJjiB)H-#u9bXN4aOx$D0a`nwo8!^T8KSa@%gg9w>a$P?09!i7zt6LpOFzf+I&cTd z$Y@AjfkjG>>ORq|&_R8)PKb$y-Q9{cf0n%geL)O%m<-F7u_07b#r*THZ+&xy&Mbrw z>66kMa;r|L^sFtk6}53a=$)3Y7`D(irR-LKq%gmp%udZk&{lR@f}<|LCZidA>a@+` zeK-XlJw7+SyFH`orCHcfd*IK3l}lnvZ2!elm^ZYsh-w1zCu_?w>l{U zO%Wc$S%)7-A%|8q6ee>e0VgbvpLd1p+!I9~K3bE5f2_CIN3JzmP5Ihs*VLO5w zRd<-^IMQnvTjE~ew~KuOnwLf&MZNsTI$p;vl-A-NA_;WHPL1^xJ=a=>X4g(%=T_>* zhgWdUH@q|ahPpbW9m+{{753dea6qLI*%~=|?kO`{6kE1tr=(jF`zdEtLv#y$d@RJA zP*>r|cJbk$)9Tc`fLt2Qy4mWo7wS8&f?LxjhnbjB0h*XwqjR^dA00lx2cx_#Tra2g zzKymWwI-J}U%>{mZ!g7t(l;4)xSo&SZ=8Nfo_8y>$U@e{U!D7xz#*}l$BxaAwjV)9 z43_U&`Td=R>(wm^(e4A~2#1FYDf`J_SIpcn5KX z;hAg#y7U4!$)QQJ#mlHNJp*ToMD^Bsl zQ{)CjK&@`)XGE3+>DI}(j^w|p+}4KZHxYJKo)<)VtF#$&$)#t#p!hN4crG9{${H+) z7XGCLnLyCG^bkA_*fxCcsqv+x6KxybB@#=RUU5Ql+fq2{fj+p-0!j}jH5_8FS+g7M zn&yL)b1g+J7sJJ?II%b2PxpjUUOY{MlrL0j>BBC8Z>fO%3dE z%J@++@m!N=`D@W|NpT*hNeV$D!gU_yly}X)Yr&z9tJnZ%nzkn{OcHf859v2m0!!Hs z?Pf?!TBN$0rI}kV>Cd`%WY=_TYAU*gqYMA4DXIXXX*D~7;V+Ls6;kPb!h=dz0%8z?|UP@5|QZ!iU4@(sx<248Y}eILDb@P0YyDqhy7x8eVGeg;JPMvs+pglFZU~mf)Oc_d zzZHK6e(N}KC{&JTk)KS!3cZ0fX_As{m-2;5CTy)WpI_aRtEj0_Hn3jS&ECjMRkr!R z7=jokojw)RZ(>WT=Pu!+r(#(c?-ohVAbe4j8!GQDpui%pP}=ZF??yZJS^00Qb-?oR z?^9v-wRi#_!iM}Kyb(g7JMmhJV>o9zu}XqX#p#U4$&5PJMDZS2s{2x;H|#11#sP#p zOP~287ADGFgL0r?Cj5?T7X4;mE?-+0UhvyH%a&;B!JvWH=B1wvFYItm1)3kMCIe7O zqxeZ!GTTiT9>veV?OQ9YGp%MFp;+yN4E?A$!rWQ2JC``wXC=cScJL}6KVDI1gfJVk}gg)X?;PZyDM0bk><Ho0ce3+CM2`czYvJ|1REP*Tx7N-iV#PQL! zKKx=~uVpO|Yxk*_#V@;^@X-#we&*nDySEHM$ZjjKQ>2ihHqd`{i&K=E`r)DDi-?ZQ z7jYGE_X_>_rE`v|^<-Fc9#?K!6%x4hJOT{t%6gD$WA7@E1QFd>KZ4nxOEF$?Q3630 zUA(7dZ~jc;o@Sf#CTfJ+hrOS>lU0}NORzNbiMp( zy0{B!=V4#kKzkVfOXCK(KbO?U*YWFjtn5yg!8d?dxIH{+@C-c62wZ{vvvdvoMyYP5 zxLm29n2a42MT7sZrBH*g0{rOl@H*FGr2)IET!Bdg|G1MUrmfq@o|`rmO}^ zu?9x>j$Mp@JimPPhi1I|Xa5umV|vvsQpwi8+S`8+ao2Ff)@kB`Zhx~h1B9+Ce!Ud5 zfN>yxOQ!FeNpJx5^Ur-NK$P_%$F`M@$JPt12QkuM5=zffzrHJ$Z_{1=(4~#Ppkq0) zxssD^tY^efxme;Eh1OTRAhX1uMoq0$qK$0?M>Itp2mRkSK62bmpg$eoF14ty2UZ(3 z1^6whqQa(m(O+I3W_3Q?oBkGJi!ocYI}*q#+yi2lv25RvJk%Ej)lU5u@-61{cyF7= zujR8337x$HTpY?6=aL1>7UJp16WV>YRPR0+4ltnok7zTCrVf88j>4D-@Qa&EZC`lF z*+XG(|Dk$zg0f1WlpMG4LcqaY?D;W+ggf=;ELl&Pn1*dK?^ei}$h$(RE7x%N@8`bh zVipXpq961_5UljC!?)2sCj4z#oEMG6uh`zWCiE20ZV9{+d2`K&SdL&=NFs4gbUw~d zFwEcP6eT#Y7i+=!g-Dn)QTJS^^W|amWf05&*!p#vw1b6wupRi)@8Yf7*`qaf^dJrsS zT3=$En3A%TqfW@*oL*1!SDY&qIDzWCLaz4R%yB=D{rAD6KTkrS72;-SQ|S zGG#a}9PBApLUT)9V);~kOF%+-;&vqZU}AZtywreF`>81DiwG`jg!L*mqJE@9bMIRN zcBpR&c?jwD-eL4E%TUYE5Kv*%x8j11)+*cCe`6Gt4D?}`zCL#KHa;g+@}@j z-{71}2}#7e6n$ho@hugdtDK<69DQM1}NqiUa%<|t3T@9t^qFhvj4WLv%#HD8Y7Xcm=~ zs$@G1hi!ldz4ws~dl$xx>t)j!ufX94XtN1Aj{Z~=w?`T)%w)7%T;UkQve^gg%!I4a z8yu)w`^bXuaHh!# z$M!4VhiZn4GtY01@3mUga*N^cho;l(JZNK8^#)g*O=Wqie{?QZx>+7r0`&N_=#w(` zbEo7z6Ng8@IZ|$h;2;P~nf{y)pudGWJ_v^t?aNgg4i z6sFRR4Mzrq<}W@2Lo_bWAZ}Fc0hySVTS49giJ)pE14H?1f?j)+-+{+|vh0}#_p!a#$S`CFspss z!LC&5xGvv(Sn&gnl($+ObHy09oJ&9wK(#)FEORpi(`?Z#D(J2gTaZT z@&C`pJEgGwfQO^8sx`nj#-xr*;l(O*!^86-aNVHXBbRnV4@Yl4_$O9rw;)*(6qDdr zMmW1yC?Ursm9UgXj=d_?@4OHL$+D86TvnGk%b&RU)CD`;HRG=Va$g8%f@6f!CUm zWg|?!mT+0vYOXVRjaXN3$&$)THOO=NnFRrd4Ulw@@gzTT22RF2$vhC!=4Mt&g$1en z{br=8i&Sbx5d$>JwcdgY?2smf`Qa40=mgFC1V4{BvK}r?3(aztsgUWp4?l~~JyhMG zd?$m)+xZo*Bt(QC5cUC1ylIVenm`bZr+EMTFN%-U8+xH>;~TzXb4Nuj^}tdjWxxv1 zaMSB;7oPM~Ud|Ni9!YJWeD#wSU}O1niKf7u**tXUvHKa>ehM!{_?@b3hO31%Qh2r> zrOzb@UjlUuni!B=`dcDd?%6+DBmIMj>1Fv%X)*aSOZks^E;fmy*?ZP`)U@DGt}2zy#1MV0L@y5SC4d`-r*+M$Y+} zBNEixS`D(Y<#M_VNT3MDc-2*UxcWU!Z4&uf0r(J^ifT%*~9p;F`9wzUSiWJ28R?&TupA? zQ0diF-cRosEQxYmrTHk7u;Itdt6t07|BDV^?u;_Jo?jyzC*_p)0z@K2Io2;nOTf~~*#bC92bO=Ft zJKkuA{ATO9JOl{vwXYre)a7uBUC)5si$d>gaBssJFmO8r0tQBb!*-!Q5}GQ$aAy5t zN?GOWPCc@{*l^xoZCjKX@jluU4XbGsaL1RH|OgZSc0GBv>lz94NhUFSw6VDjydkhmC6x)US!td@UeQ2&hLQUst+%1x7{84RPXzVTHs9D*)sU!_5U@9qtj2oF6U*iW? zrvm>5Divcea4fy2R3I${r`+O*<(^jcRcVD5D{aW57g}T`_Z|~D5%yf%s!*Ut?-Mk7 zyGq=^*B6SEz=y6H+mAwd=v`f#Mo%q;R~aEW5A+xTl*wf{&6d`ck0T0Q(|H~@J~y#H z$iC(;tiSqfa0Oj#t(SK`&rHiaA-EDBr`@}qI|T(;1c7Zyr(~6cjJJ}6EJy5|E#=V0Rxw^)Ix`S zOe=-j9QT0&W(-)CCMkpA37P$h431i|SqDRv5l9zUoRQ8?0n_Fi!e0iKK5YUBIvT3o-BKH5r z6({XFF@GGq2X>q}wqT-CWM*eL`*-|~_3_)fZx7g*Q#0z-#U1(Gh4OS536qanBB-$} zA_UeJ_=m4$U!$hRG$VI)=VWI1KPU>!6$S+$@@htYa0y7}<(DT6mtWxB0*2rB^UVjR z$`_XFB&B8S+K|(;=h#lyiT<`mJ>UwhkM8ADCbe#$i4%5}WXOCK(-Zi-D%Dchx^+bQ z3hfF9w?Q&ei1#|6}-pdGY&=zawGas)d0< zg-{3vNyF}vV0!to&2-b=0XM&mV@9LjTrWhw1&4tqr9DW6YF5y|E#Hy?UXV0ZsXr0I zdGB8h*6m~{)9oaHG<`0t_~E(<;LFQz+cE3GCe>!cT~M!adfKe_gIeV7MT+IZLO|Rv zYyW>DM?F31zeLVad^aqSQ?TrH0`n~XAo>NO-p!-#d7ZEC2smxFuTk8eSR901No-r$ zFSY6ifG8%GVO7z>9u|}oY#O%dTl{IXkwCS!fqrB7m`|$bbfK?`eX6lE#k3)VkLS?8w$~nUi81lQ|+&>QFc=wB)?I%Fc%ZLiD` zV^Bt!^fdCztVagIz#_4h=i9%x(Zhk8b5955ToY`exPa!jJw*as^f#Z87)}dftKRo^^ zD$3@+P?IpbO$$Om_*=&b&9j7OWReJ}5QCL(!rf7!IXyxohSW$s^;+TUW`sC~^b^3a ztCB5JK(BtuI!S(D#OIh(JP&{Nssoh61Uf-f$vRRuMq}Zw%CmhSkQ6!kdB@75#~K$U zg{q6T!Px~E4rf`W7@~g8qA4id$D@&`h$|sJGDe%5O*~arB=uyY2Wtq4%CxwhNfGdF z51v9g>Ew!!_>CmpWw!AsyvMCJ{{G#TqOg?ZeDZm~s*9vk)xzne2!?5Z?)^f*Gkl*f zLQyH9l!q7bSJxyn!j=+Tv1JRSmOv{Zo6BE87XIfndZ{CSwy#JQ2BGxis)7DtrNmuO z!%1*^#6g@=V26g2nQ8%@HFT*= zhW8JrJd|RaLT^+JiM7Uo6npB0f`d!XJa5Rfu0B+nb#BQoB*yb9u?G@4u`cj<>Z&bE znr}t*x|`{UP=xwWer=q^$(t2t)nHPE$LelJiUcqlkfY#Pp32p0x~3|l$0Kf4rv4dX+adFJ4bhSHzG)hpfsbqk&^BhjLrf3 z-M-%6&+k9j9*^z1&+B;}&*L`r*G1FbM6%`h=aO*a4i)oNtWyx1f9fAeDEMUcc(kD& z49BSVxPg)nJknB=)kaXN(i)!0VY zXkWGdS}Y(w)f;=^G&=>LUUQk1 z;plF=u;12dXB?lgD@~)iLW5#_8hXTBA;&y8Fc+1HT6~+HRT!d*A5@% zL-v{jpa!*eL+|QiABK{Aii zi2kLVGf>~*8Y6=n@9vJBObq`QGqwtG8g4O8PU`bCm(80sXCkNIW*bK5crB*r3~|{l zj+vxeW#_v);qQy;EcV(Hr(3R)03u)?{ViV!y!lvw-LsgiT3o!nMrl5-Vdb@d;1Jj9 zoNP*UQqU9c(j}B=zDw}jmdWWO3z9#L^k2S&FlK-SYYJ^i^NnV6g za+JfD+HW^Q*^E$4>U=T{bqEEj{@x)69RiwURpQ4!08@T<-r)gD^j!MH`x(X-Rfh_6X-p1Vo|rZ{`v7=+!rSt zEuzq;7ci*f+lD7&0l_}>Jl6!IAWw?$qCA*COF=hi@qN#-k(S+fUla>_$=2g)TW3R-PaopUBS` ziF9Hy|H>e>*x-e?-!o^frXrc`EvB%1MrLGe$JYL50(VdlaO!E!aVdUijkv(zB@bq>You zh;6B$9h@1l<)0{}+O5RcdE)%jhyhpvmV02CT@a}1qJq+JEh4+`E$}eIZjA}y}T-6Mr z1`mrbPCi61&njb9^|JQ=72}^Q=S$X|%nqgPiV$w|YMH zj9vKkT!*%8rPKEo3*CK;ZiA!ar_n2OxC&0LJvsrduDRd-ka6n)A`)a``EA))Dnmd(xcvIO-7Au8S9}6-nQ}ku&_R9_*5i>LDLVe z99M$-Hw_;r519qKS92GUh=!b--Ff2D@0!TkkId5N9+#+PEsD2!$24a%)b-W8w<a6l530aF2gkhKJvKR4X95So{7i ze^g;f>de0ZGnAgJWSi$|tOWTlqM_Tn?OlmxOiASCF=Nk)Uwi?Sa1a3@9EqrP65HTm z(-(E6io2dJ6wAhAi61MBW=su7@U#ag`0+m1{`Gs0{RrbpsVpB(ht#~G|kPrpJ_&x$*i-l%vvO3>Dbbywigo}~*H;O8!ga!c_b73$J; z9X8R)X!!H;m5Pab%F8Q5L&g0cC$9d=s0R7JwC;YxX@UqpO?Hyi?B zRmA^}071H*5Lo#UVjATjm9`$i|7IRDcfg2M^>&gyquPcosD@}{F$T+P;*m&Ma+vNX zgnbC{ve1zTfJll#a;np3`8-`a2B|(gvGn_rSy8rxJ8G)MIN=aD%sH(TmC;Aikq6!! zW7{J{wd96l%@Ygck@WHyokd@^Nk5p-Dhdf7q^P)sKNfs>Tq1ol;nkZ*vO4&5Lh$fRV5ydfb_dxg;v9ax2vpZ~1D>c>`?)SK8qQ6n=wj6o@nJj`!Ynm`XK9#7RP7&F4)4rZSrp%rjb5_WR%gk#d3E z@$n0yRwf^#^!?z~19W4Kc5{`45~7HAV_)cX8(y1>cK=U7s6g`ath{ANmiluW&B1ojlx~XN=WtSv_e^CWw^8F1pIbYR9R|>%DJ%BJtd%epv&^@6+rPZAmGMtqhJCnLuh7VfHwoVbYWy%R|Mae-=*xo9G7ajZ zsm~YJ+)q&FQ3@G4#({vWBFHQn9tSfR$;acQA%@6^kpFwxJrAJZPAE8+-{zgEZaZJw zI}dgPviLw-_6N^G@JGQMo`Tvmaj9sTgTu zIl)I(v(vK?Nl~N5)^IO};1T(4>2N&sQp0DM?z7eN?qJ2wr8g2@DgC@x=P@T;5}8aT zPdysk%~)F-kt<`OJoV;W5~cS6bk**lhczPU`ecSG6DfN!zkCwEm%jPHkQm9uLrC%* zww|sS>;!|E2YH`tr3L#*4Z%TYW_M!@Uctyp3WkBD?&^PVY?bD}Ru7Pmwz#tO z(YD)n4^sEu&M1#P31BGkIHc1)2j37UjSFJM5>0!^qNqLGP6q8J{+!S<5hZAL`oe8F-y+ikH1jA-Ay?=d?jw@Uplc zK5$ypoBw6Y`GXD?zV^v;pyfqM7`i39TozN7Q{j)7oDq|GvRi`5gXbGOOuWl-`s+hz z+>_&Osl5H(SCg;&3*zutB=RqQo<5i7Cb5fObTl*az#UydD@MD!+wJCR*?LohSzW7x zwN7aHSM$z?dz3<+-R{-T-x|yH81tF^!Jg3mL9pM~teSB*Fv5ztF4bF8q0mfqSN9(Fk|<$l}|)<9?9043j#mN?FrU6c%KUB1EHkTv#P+mz(E*SipC zsSvrqJgz%(GL!5Nar$5>fZRK2Wj=`kFIh778h+K~ph+j1z9iM-z@eiO{*#BlHbOD+ zRA!~9qMGs17+vx#nRU+v37MxzvDrW{Mcq@Qa=w43Rzjj(+eTchO1nc~Z_D+?&;fwN zGcaD#?HI%}q|Js@(h($9Ty`pr_Z&bXCct9}WuO1j64YI^Q(*k-19ylA`__AhNB6rhVg zd8c{B<)Sj*9+T$_d8%Dr1vy9+?I@3FIJ_I*zEE4!XJO(7d=2Xt1Z2&L-=w81;a>q3 z`Nv~O=T_b~7P(RF{53dXW6CKD?g+VBf^M!a*`WvLLNhF zsrQvAtmN5ElCKU-6f{)(1ZIskF?Y@Q8rqiE+UAao>E7^k+VcTc{-R-#A4uSG8K-At zNX$tWr4K^be-l`E>UF5EbdzLhC=2Gy98E)y#*{(V-VPn&cAa~wBtRdmednfIVM`>> z#`^RNs|H?Od~oGR>k;)TURls|a3L5|vh4c-LirqGclipSxOJ`;1x(i;^B7H9_-fib9L|DZ>s{w1*JB|qvgD17nIRXl z5YG>XA-^x8v_=6f*!(=y|3^L~0x|SbfJPrTt)W*_^ zr$p1j44c=uoLK1m$xajG|$y}{+DNK$?JA0HWa=nve&h?`#D;C^56xJY@n6Sfd^ zqPGMa_1%rGUBc&j8+;2{N-i-=7p|5K2CDoW`xV0o5a8_Xh9Z zwvHN*RkJH2ucW(YD(k+JD=SX5&8x^2vO8G{l*26wHP`nsR%K=x{;+jvxb%TVqX^c0 zm4pl0&#i;qamfR~yW-@+HpHdZK=cpWQNxJu144XBc&5tr+fn`C{fn*Z&F(et)1O@2 zBXeH~A2(s22VBN|l<25@SJ%H(>zd`=@zr$TGUj0I5+I!{pu}(Zx(&EniLM#c<+p*rGvM94(hKh#f$Bh^YHRp)C1^>8>IjL^lciNeC z^HTjs*Do*p|5>tK)O8Hv#bdRl`EVp>;f7UK4)fg8gyUezrSu7%Ze`~zKYrU3BJ55c zoXMPe$1V{`^ic?>0K#8LiX7b(c8YCq*IBXH$sbmb&zZaH9ImGyR$6FG_T80=1g9{D zqPX07SDs?-QL%K}$t*r2nNnk3K?vntrE;tanuttdWEL~_EZSV6t7Z*!vv)_(%Sr7Q z%ESMA^~O!*cQt#$@JlRQd|>{L-Du&vF?_9@MID>A;*@fWqxBb`-}D!>ltP<4?tf2! z(sw`)XxNq%*#MACz(HKQWtNh3-n2KuU!zut0Uf-EO#KlXMY z)%`Kk3A{Wm%^NW%u9T5|Q=DsO>{(}wSqA|?Rr17|mY_TCHRx-b=$n=;FRVB$nqLfi z)Rr#3XFKNZ_cDc8CqG(7KiRh|dkks9Fhje-c}z{r>sU16kW=~<&7B51CVfcmyI-=v z{yhXH>=N5z5*!6J3#8aqK9w6_MoyI)%|tB$w`w(I?~>`)7Oa#j?5n-kKR-$b#5MZ5Q?Tp!w0 zxf+s875I*sXR&57yn!DRh`46njp^wc95liA>MV zmcSn@S!Mk#74%~>MsXrVqaTNqmamNDpBQ5^%A06ZsH)KVxY5cR_ApcI{@3D^D-1ce z8lVPWenn&}f?3y;!KHoN?uUmfPN1f13A>@#mld!sWuhxt$5-?w51Yb`NyHH(1MG2# zKY8NRKHKh#hj6>!j+_h-$d8J6p@11qnP1Ft4ZNLIF%t1 zh1Yts40?EWJPU=NW?Bk=xeUhL&?xq6zx==bo}M_sJ=DJ!`v>wL{)aZ4#he@vSGAFC z2#=!uw){xKvVPkeu_WxexbQrqrX@ig=HDHYq28?ax9<4U^KeWVur?#~Vx$+mc&&HB z&h`Mj$vE8JfxXSb%TD0RXqc^toX=cNHk;==%^noza;ey`ZM3x zIBaHCLQc6hkdnm=_$=d6D=a=9ffh+}^Zh7{^{t-AfR{9!^5jcKN zg-}@z1$Uy|J8Lq+E$@f5dipN#M(L%7CABP`o;4-49(=4IWDywx&&0rAA3!u^g!apR zt`PuxdEl_AB6SpN*UM-vbZ5sEtd!tFqa+(dE;q*wiHlPj&F~?ka!hB~O3%EQTi;W= z0Eh16RU)wz%WwxQ8 z(Cga;f%9eQP4*lcBe=5!-Wk|aTP*q5r|`s5i961>?5dR&Xw z(YDe3GdJL!wRnuAc~wk>A=1z3PYX@d2pj(Ol?D$_uK569;>~-d%;7$=iZ~$SmjtfS zHeMDjkiTomPas!R26^@!Ur`_``*rB$`~WRxI#TC%Z84|j=f-|; zd@C5@|GwhToGLCq+CR&dJl3C?1w5Et4H^u@^5XtE7;5THipP)3dGkTnqWy+0zI0n^ z{EKpb%h9LcBWgxM2z^wEsJ5d#Otxjc(P;pR=u`Ks@;=boY1)d?d)$Tj>aPsRt%=)) zJR(UC)w=M5^+ES}3e>kv2Uz)C$mKC=DP=gj>$heb@NPg4TZEe}NIY2d-224c4;_F< znD^pzy$LrI%r(&>9*0NBvJ2d6Pp!6eKRlk>)yh$ucVFwKMvo8@(S({*$BMS{NlMT_ z{^a7G-5qZ+{8%@;icZSgVQ}rwaH;;G@46b0j&KsLZe8}>Yw~l|TXHL#ud;g#;EH0~ zF~LisVPhmujcj?za(`TKYZDjB#HXDV$s{qQVbZWHvc+dZnjB;HLF~9sR%hVO&%J}z)zee2lIOqd&E zf%7$?Koja0Ua{}fN4h}6FnZ;D+w0M+@#gSf;XRr7V1T68eyL^taPG-Ui7|if%0&6z zn!~+WOWd(Q7(HUyXI=T(+@ay`25O&sF1rQpTkVwC<)9OYf?wq=EZgOE`~8`1eUrP9 zyBZA~1-3?EF8PUX{mFm0Gj?xUY1}rXoy>>$&pWYN&Fz2jg>qzW{}EYgqi7gZ-xiTHzfY<(;|eOV%fJ8BRsr_Qz{z+QTpL zRTBd9FpfQ?N1!@(s#CTCB>v(i{RY8ny0K0>#03S#cqc{f1NChM3_aD-0OFAMW3j?` zgC0+xufF&l#JkJ?Q*vFQzYytvzacF0pTq`@6vZ)C56n+RUwAnaF-h}3^lDas8c#gC zJ}|ldLIDQpb$n|X^>!Ow+a^OPG7Z4qWN}#=$+^cby~`!tXy7SSvWOWW`?@Wk^%`s8 z7g=qo=Xv1=<~DOun#2y?O`lQ>^7JZt#?|fqEEtmSa00PXbAi8*6s(#Jh*gD}edMV^{EW$^@PTI%C~(B2A!LZx4Rf zS}#dw%o-O4W`qr0^Y>fP(xk0nbhC@gVdpU!N(G(!k0vZ^Y*5&tnNZC|j}a|hH<+Mz zKTRM@l3dHSHmYv~=8g`#RzXLN$W40+Ry`iO$0?PGM^7jn3+}rj zTpFWV&OZR@VcR@oA_=2>k>L>dJ80@2r;b36!(YcK_C$roFXtF3__Qs55#$m&py{yh zeH;YaQq+-FdkCPGwp_s!cjx)>Ik~gXH*J?IUJqb4yx0-IXuLdJ`?Z3c)$FrHzqon~ zKV2*aeyEtpk^L{C&)1*S23{XhGmt{^rDM%_S}v*t|AQv|7*=mFfGp%|arq^y@E1Mr zQHdY1@`FAb)@cg{W_%>#QhEzf4D`)#QJ_D4^c;iPop<*Ou!h<8L}$V>*Cs1mv>*{# zzTX{ysF8%`?cQOYHN)o`Tw9yh5b(tw$aQw}{$sBorm>H5n#77J@g?3{Qjy!Cf44>@ zx4~C;0)Epki*dKS`jf}r@@K*eck}^+@3eD4x&}Zr`I}lo z>^TBG51*i?(ec{|yf>p$Hi_Ht?U2g@ zRkCOow!m)Anzks;TRLxB;%^2|ah94LixL#;*XX5hPB13)l$bO>@zvkp) z?rOH%aV;N3J!RTvbnfroW4gyMj0=2*yGl5y`OxYL0dJ$Y9rxyaxMH|r2eOL3umMAo zB3$y@G3vbQKgX-LEF{681`lPA)g@=6-)@!O)AjrHJwYhH+)I7;ztHkd7k29F7sX?+ zu2cXoJ!-D@UaDbh!c6P-tGfY$4B(-q;kTCh)B+U!k=wJuXYEms!TPBFufO-bCtDz* zJml8c$6-uC{U(a5cj{o;%hBLmzNK*zML)7yoeu;J;pg z9O20T-ACV|I6B&XvZ&#k0ky^WL~-G`|GZ=U*%0i`dqK;#XV^YQQUSEXdX%LELgcGH zN`%^2c4|0ypU?W~;6L~kH09X`SsR4M`VzW=U89k_i~5G>Y@hVEzp||1SZ_=0GNOB; zMt)Ny+6m|J>;s(I_2qVNI?4br4A<;!^D+HQH6utnpo~qzi%< z!fB!hlHse!mp;$9FcQ6$AlM9+Dh>@C)=eb86@g=%Z@EIqsE}%_5XNCIu=1MYEa_={ z?T>w5kDe+jismfr-?K?)@VTPxY_$7e77V*sA0rAexzGJ;;Vmr(Ld&fLnjfc-EzDPc z3$i4`^g8<3MWlDjQKGEA0jfl+JAE`wkDmh#thVL+>iCyC`D2e>1$Bkr+0@T{WL^$- zc(2LegIS=dlrNhXvs$ZJw!3~N`a6P@(ywiq21e4#2Q~Mw@QEJ2QoO-7;^dt*o>93{ zc__pnLSt}FY#Gn(c?aM7KNmn#fCcya{xlVu^X@ZRMc__Oqi&Mo@C=%n+5bOWpH4Bq z+S>Q~S=PLKAa))FiQDk{1yf!so~?3`o!qZg4%H9ezn?~uDcX^&;cn2P2~-7q`Pw-v z-W0&+d5q;8ikDyQUJPot2w~#xm}<%rWqQgJQ^YUqFt00GY8|@O@!!1e{r8{EWnI9m z-)di25;R222~R>+j^;V!sQ6OPD#s4Z`}1)5Ck`18U$C?FTR!oM-3Bd;SQQmf|3d4%(M{FnY#Ej)^0lX8sMY*Z$NoW`$y$ z6s=w0Xq{3J_wHbdtvmMa;EO#*#$NB8fkSw)zcHibMYcHy0p1iqFV!L;c1`+&`v2TC zE=Fj~fJNELPw_(H5;;lIGFJ(^pslwWp>?0lac{Eir5m!mys)9VmpT72b>+?`83v7r z4)JFZ0sio^@g=fPWj;G$DyunOyIGol)o?Gtn;IPiDynxkQE2hTCJUbwqmZH=z#L;1 zB=a_g8($Y8_pZWCUFP54U?T1dX&I}0F)tZh(ay&NSQc%%7P}H5)hl)uF;DkL4n(Bt zS>5~}3?J#X^@|^<2n1HzbZ_0i)S7=NpZi?cLrC2?73tG4$^iVQ^4;%H2CR}W z^2-|NwKSz#%aHASTTkVJ3y;L6cNTKlc(y^M3YEAZMbdj#6l>B%N=I@T59Z*Sds`_X7JmEDmL{9sB{LMK2%;l1Z?@W?jJ3_{9>;(G1aCro05*} z1^6aY*h&Y3+_|S5K-w2qhRkNf;BGs;g$wPA-!oH#%|i?P-#vVg`Kn4;GGxWBCJNr0 z(ad9xZ1m^p$8T={Q=pT|ea-??>%KwFTPf z-8gFZ!@eoalZdK>Fjb5$up|_{y!l>UW8j>E;%e{;Ch#>{sI6OuzO%Cn;~5 zB3}z4#!aag_HR$*8VA9FYMzX7OkLE}4HM4=f;67DA4d$I!UyH6d8QM=!f8AvvF0K|S_WBtv{7#BCIsZrn z=_6&qIP$ogzeC2wruiBzr0sCI?)-|5Ab=$^z+*A`J7d2S zb$>TF%>xVHXH#q85VuQ*H?jtpPX<1t4XP(8YdCm*G1nkr^QJ9xJz=mQBfNk%CPTCL-$2d|S`yUla ztFN?w&~?H?MkUWfFeme^!!K=0IvN_#I$=I0QN97u*ZA#Kg#99>W8l#rnAEJ&7|}BV zt6X1H2T^HLRJMZTd%ACB3p}P>T#R_*Vok9l9|55>BRLD&KpuJfn8DIR7<2=iID4_K zksZ)hp%IM%*_7h#G>1*k1Wv)j4;mlszU~m2b8s=bW;cc7}AZ21oKfF>nsN<;h<-=+8p>Y{%zPl|JoDn35(QSn){I2o2ij0>JT zyGc>Ga^y-q+?binIwD!LFIWH^Glnp0z%O&A?hcW7(vtM_II|36QibQ7m7r}Q_1Yh&%L`nSHWcK}gTl6Z?(}<>Qm7k<)KyN(XjZ%nY zk`-&Seo9qgU=TuR2Jc!Vw!ogL1n#&o+4ei4V-7}sZpy;ema48EO(PpJ$1#5$l{(!w zG7N7?Y)E0KvW^Lh1C6#oG ztyu_ym*0VR!GRw0s}SrC?a{Y>7Psrs0mzp1IHYIUc=S_)`;i4JO63$@&tIb!{PV{h zZA?@MVq?8`*zYCE!5_Sgm!Da@P7bR_4sw0Yp&lQ)%MW?Paj`2VYG_AhXQ1D0R^rK? z3>Gm`%g|#>YKyoM;ATeNun6lbPM(ol01c?gsJ&1Bu;zn*npl{_noNCgYtLV+d5L!@ z5=6c2LX&qt1-u+kcHDO+D2B{)RkaNJIxqP$E&qf5mhVg71dYPx?;A~F{fB@W>0#}_ zISBE8&E7q$U9h=FNc&j2Px;2KfkDLN?PopIUh#sB1qm)r64PR|%c0*j*w56E zEIroHICDAcvWi`d1!D~(J;rjk@S(I-TeXT{g4WfjeNhvXHn@EFZDi8!)@=coQ0{b-sx=f%qSjTRY;QqRT*6|*lTcz=lF?{i>hU6G%y`Uzz~&tfS4 ztml}`s}Mx?zh(}k(!9{ftqEZ$8f?}kb$X0VnHg}VN2yQ~$*fK3)c)efb})se@eCk8 zJ;Ml*&5Sl?<>9$A zdCDdrXkeI5y?=2TQJuxcDCDH9ddXasw?}=el$FEHQ0Sa27Y6&KhD0 zbA!AFgrmXf6)v8&axTvN4P`|~4BaCJJ7TEGgm?qy+OsSBo4TTbumSdFm z2vY?%e>SUGism<`J5#hIlYi`A5SG2X^xwv5pyTiQ#Onp)%RSH#OHEacUgH~vCUt*6 z|LGS?s20gPeg$lJz0ClNw`5A*N&yGC@xD@W7Rw6k6NP2iSBpe+C)lSxY!hn+qocQs zQN7)7rL;GRqFz7yAT}LY=k5v6mUoO9y*;ue2^kO9x+ zY^+ndj+K8KoKQVK`%mjczIiXN-%n;ESU}KzRP9X1M#wm+xfk^bT_0VkU{-6)`Ms_; z229E9lXedq?swty44?o})_@aZJs{Sg59wC^C*4~^R#6sWY(a_7uD%Pl0wvr0!P(u0 zC%CW2zuqh&31tiieK<;qqP#Ze5-6J83I8B9OJN?mD@}rVDDz`+$)Fy`UC;^OdyTW%rW87C@2bNj+-tfBo<=sK+y<=+Fad6Y zWIO0Yd{6phBun^cCgq72?=mimCl1_mKi3f!3~qC|GCoheJQ4y$?*QA&i6|ZyI(HlU zZPc2;SP5>g9u9I2w^M`V&NLwpaxQx#J@9arN&%9$5Bdh{R}6#lK2RxwcA#6k7W}v0 zk+UH%%46eOIHR!b-%Y#4x{3Qo60cCb8yb(&=K+ShV+ypRKJN~r-x<1EJmhXZGI5u< zr%K9V?|lhy9_|wmu*c~GF4pjzq-Ccl@#Aq*O#K#*GQ)O;(LCm@PglTsS!5`P4Wufw z|D`I>PZ@t=2oUhjdx{&UTD2ashOdFaiW%ideotnCYkErn`gjBjmuF={C!nNfhg5?c zb?!Fbd>6n2Hi=TpNsKC9tm^_!JY&j(Z`(KD>&(}ja&H0&X|43%`N%jd@37Exa|K>v| z3S?_Cx`6?S1P40gThJMu4Fw=HqExs9MrdY6GWD?!xt(I6#C4$+OJ9ggR|?K7+86rG zq`u-IVM^#}yQIk_SoZ*O1`S=Nfz;KIrX5iWNLQ-Hz843SlRa7-W-(Y1q3S{BXD+=T z2}SecSl$$(=Z)F>-oI$@fbyY|%`Nq2af@e?I3Qf&YrW{5q-?5$N zU9119-3>Ul|MLN9m0pL}mn~cYBl^a}%Np->la)8C9eIS~yHaVl8pDsh!-t7Ds8ohc zmm+*EjW#iW5-zf0H||6W7AHAhK`!|R+nEb#~_h`?i_zEDP!L{a*XvdikuUJmjQGG{K04WA?j zOf(b2cL;KPxk+M5ygF9aqy7x0^)==ojBqaUQMrCGtEpyd##kX^V$DUzhsSEq(8yRC zugr*?iN&JOQ4b_A>d=a_Y;Ze}=O$g*?DGL9Y$afU^)N+wd8H#WOdEOY-rt(FI1f>e zHu!w9i>_tE!aXz>qy zbpiduMK&8Qa{8PXsZ!lg7jZxSem3!&|$Zw%s* zxFth;Pz2HasRcA&SBJGbxzu;QF)*i|*`>-}h>HixI?nkUf*O?Tmk)@1G7L5!YiZo? zoM8IetkiS?Gjj^mWNFXjDx6}mmPB;;P(|XbkL{>s#CNq9zVtFnSYq5-ZA0xJVuv{w z(7Mxct#<=~1d5l1b9_3a&;PwCX2@6hra|CJZhg6*kr{Zf4OQB zDN4A#kwR&mSztzG$fs?tyQmJ_v^TDnf4K^* z41X%nW&WqEXxa%Q&RDGc7g{(st{k_kdO2Xm1Z4le(1MId$vXaK)CyAmVtdgv1L%nU zOvKnxmV{_lzqTDtKFjj>vayNvnPfDqj`2%ffi-FHE4iL5WA_>-%8-w)I>2p{j-ETk<@ap%}Y#$;pmLox0Owq7gk$lF@5)=O6v z=E&r^Djn9~OY9wF*no4xiuU|0fq(V))M_n{?jlyxe_;bux9&Eb}h$+uB(P%7W@y z5S=g$c#;0Dp|kyCO5a4s8k5~C6*L!BD-_tQmpAy?UJu4z-fRMT5`(ELj$5&C;fhk! zRD!x%z&;JYFv^JnqpgpWo6ZRrHfF7P=!BHXiW$1dhSS{#iFAFo;d?aeL7|YLIS6x<2 z(kuM{G@f3@2@zW6fMVsarXhmdGq{p{#UaUJJ!jyO4YC9)1q_( z>Ta?kDCl$Jf9CvK{As=8idtnSCM+0zFlPQ`E?MZTE8gO!Q z?)I0xC>_+pVtGvrA`9r!VlCb?0fgezcwsXvd2gGf0no_lKhYs^uI3tIXZual4A3Dcg#;^Ho-%kz2 zh+2$!CK6jI;m|=SPqxOmn8wD(PU@uI-l#9GL}zpXktN}p?Y3$@i4O0$Ry!}boHca~X@0-tYjIBM-3joj9Z2)d_ecs)!KXN$7yN@{ zJZZLx|0?CJ4Vd0z0FVvnso20?SGsaNBSHd-$#e>P_`WR~oiVUfA(Z7`PCEE#C4tfD z7x{7zL#fJodV_`z9&QDo5mL4uaf;tR(pfFK#-P-q^kBx=jEV`!%(j)~d3XFBt+?Im zEq*dML33+aUH;t*MFr5UDg*sv{Dx3bJeG6*;=G6pW}3X&-2p)#)U;``8p990%8aG| zOi&*$dNPWK2%pdJq}Xm7a?k}^`+jKqd=ID;soV|Ck z-L&KTEcrsy@7TBSLc8NmE|eseSyYZ{UX>SAVV92NNcvDvUG5epn*aTr* zi4Lidp|u`re50VCU@^Y2JL(NGHbFB7N5AJHC1zoTd~ZU_Y^Uq&Sde?s37wnsqu$tD zG7EkXy(1n)c$z1NLTsFK`Iziz{sO7-qpnZ>6|V|MY4o?9Xm6FJqyXgOc*w=yf=XO< zosJ%vB@GwePmL{AQmW0hZH+PQbYxqLTeYp)HH$4Srf~b_Mu1Kn+Ptp#Q2b?j+>);? zy8O=e5l*KjMt@uV?3?$mJx1|{O?r<&q?jI=Dq`s|VnyO2Yl&e-#B1`DgkR3OxwxNL zX&7ntUyp}qo@=sXqI$5p1x#IfpXW6(# zwsVwms$WXTsb6P{a)eT2P{$vv_`r%}JtU~LsW6P<@j8>R2>7MkNo2l8#91imz||Z% z@?Q&gI-}Vp?`PNReEO7wu33Ua@a(E4d9{WMOw9&%UW>&f%@BRoN%HVOH-B|n1VQE= zM&)^1HhY|`Uv_WA*{AKzwqKgKFMH0q9R<1D+xzI<5vvAhwdI=fMMW_|L1+qdp@#QYd*}Hd*9do zUDw__P`f;MDaSg`$BnVraj&yv3e^L2EhCRQ)9xAnadUx*z9y}lgeYnAle9li1ib!B z=H|&e6Dc$n0z%GR_Wl^{FA%d>|0@{)6r_Nql(z!$Wt*(4f?~XnMw^1y{;*+HEP6Lf zpXFoGzMX6CkjeZ*vS?df%JR#T1&-j`+m9%bDW?@QLRiR6)2hjC=r-kpOn1*9he~85 zCtct{W=HL@N#smW`wy9wj?VVU-K9KYN-5pjh`G?%31xqJ)Q!ieVbJ9THX+-7v@T@< zbikEl&!`Xeg-}cr`6+dgLc4jyLLFnirKvD3B}t9m-{(q2!dZGKSky{QRqg)R$@uIo z^wzX+Hg^c%ZO8(7q~ZiC9hge?Y&uj-;#~Kv=Q!@-Abu+Her$80)8OC>>{wU}FkAn0 zUJd#JhpnBbiw_=Cu035CfL-COfG4ce{ z&|uY#2Z4qFZ1IObAt3Pm8^QZTk`Hy@;HL%0v%68a@I!+QaP@ID@$u$H3UMe_3r*{b zei{X=o`SW5NM#OoD7R)GSSg;GTUT2U2brLsfeU93V<&I@YDgC#1&ow%e$XT8yb|Es z(-h!t{^ZE)@$%c<(bMHZtIYxB)Q%@7f(Fhns~5ET7<|_xyZAU3j8LvBmbs+I@)U`4tNkA;vWQ@ zDT{B?)3+g~QX4>2{EWF3$!;GR0OQ!?c%c)NY}RjS6t34TH@Uz&Ux}C)AB2@(zYh{W ze+h<3izj_LH#c0r?+u=M$2c><8p9Mbp3SC!6GreHif3@7y4&O@f3x@=wP$d0$@dmEP2mq1-Ohmtx(D-(<{e? zZin_Hb%iG5l*j~6DR4iom86=leb{FG9jRbXQpp)O_5O>2maWJaaGub5#YgfEjdCKI z^-DqthQWr>xBjDUU-kEF_9P&<;Aw$oOe#vs zy!i^8dy$KrUhupq7xRI=v3xVGe!ppLCaZ@T@ttD-uEznbPLr^(<4JeW4&03Mg3gVh z-#7Ks5`?+NdTtNZSDq|6>d2SGhI2)@;<{Msuq&XqJgOXPo1e(%Y?UU<*((F@XXrLI z2=*gL@GH`a#cvtymVR_oP2uR&<@^SL^TcyFz9 zUa2*>>E}XQLF6bozUH{&E9>KPA$Er-+7uNg;40=&O{~URt@fw=>-Rx!ib8;|Nslb} z%`3lG7}-@i{sISwg=o*qHwBtg5OzIvjL+Hj{>W$iiKnH(Ta@A>dDUh=RRGzK>0Q>m zKSpGNS_rs4$HcczEvZ|BSmU4~RcC+=N*p`Y> z?^@u?R0b^ie#o`Lkdhp(Is77u$i*Y!Mv|4O?H#I@=N9b4nn7EXn@oR5vj2j(@OKpzF!E zyVCs-R3U+@GAEGKjB0XYFWZ^6Z;RR%z(oQ{Oy;o6Y;u^%+p|sx}D|A zE{lmF-EfattInU8!c2!IHJq4f?qMkOY67PzQNt1Bq`mfZdT)9}4yH=P;^10a12T%^ z)+3y6o=04VN5mx!`s?Po{Oo%>w>DY$Wt&#Hkqz4Z*%fb-YHry}?`t#s;q)8l2K0~& zjp13%e5#CAq4?TUp~mp5N2aRN&KKEVSwrwc1%uU(e-2cLQty6oVh0NB0bBN*ss z4?rir^i0spD zbW0uEghwge6YS~i>&g2`sU~ST-^bquU@v$}oHu&*&h_J`2dCCm(5l`OD^~ij=XaA1 z54%pcox2B_jXv4C-IGG+Aa)hRFu!aAVs0|f%l|kL+~4up#bbTHCw?K6_9*V6>3V~c z3T*~GE+DBFif>scMD(!!>>$u(0J&B2Dkwfc;2QIUm`cACZsA5JZmB|;bhn8vpxW2< z`p?ydU&2=cd7D7>H9xQtF=)$hO(HgbnxFtxhB?ecHDvWG!4b|u6Z7vPMGT&RWGgD2 zcGF2{3M>!L_K-h|@aeSH>#zXzzH5Jnnggr241aRy*I`I}3A3JipiKQc<}6D<&+&6* zD;aU^Bel?Nlg&f1&DHd^2)S%^)k=A`VJ7+FlpSPb4y(gqsZofrzbZs&F+}}=EP%rfb?S)-z0GuMQJrw%4FDgPhj zC5l;zmVV5&^O0+;syouil1eVMUTYeW;I9IoyCH!C z5|Z@3zV1E_J~hWSc#bBx4zN>`e%Fegv}o;BE~oL*D*GjEJoSZVpT{KzREqC=b+kvt z9@^wl5)@SY%MH-P^k(H=u}LpEAE$xsL|`g@sC;Nr+DFTI zY84z3!$m250B%O2I^!B0Co3?1QB-HWRJ4MX|Lb1lv%mDzeIr@wW~#VbHG;5D zi%T-%6$wx)+a@Ti(~IR?Bm-M%`k8%OJ`Ta6RCyO+l}d7P0nEDT$qzs6T0$?MTEXZ` z$XZPboDc}yLeTNEJ=dFSrx`ARR5wnGW9u@>KteC>ax&sAhlcx85z6gJg4N95f7ESL z?(i}?=k&es8ypSUXWdpnOMwhBOq8UGskqiHhDkq@{7}5slP;(XkRtSaJ(wlk`v5h zq8}b3E?w#zINRj^N(FEAB{~8FZkvqnXMQ(bDfioGo{x=D@^uAt<^_4)+TG7=xzf{% zNw|FW>Ol$;yw%{xKY)k38+7oKfBQP+;aJ~65IplB?xKM1!G9ut8Gf~Q~INI6aW zrL!KL=ktN7~f@{k=`x`L!bWgEdc7Zo|t+?Zq3*ovA$@p8M5A74j7lEOBEm3`q_&oI!;+qLfuh~ z=78pdwx>j+*uVUPQ$vHq=dlG;Bj{2u6yuCz)sE`dT6F@>rpI(4{<|`{dUfcIrYC7B zc-^A)VO<~PrSl@d+)I^Q>xS}9Vekj?wk}llkT^E%1UJQsT3P4LOxHdfy3d1fHj%( zwI*b%@sr3Cy}3hK%GB35BqjJ@vfgLW8r8$ToWxu_?I1U&_Mz7IRQh>9nRMtXuD#A@xm{eBtl(fX!_vZ zLW8~W8@ahl0u1PZc{qxT@^!FpYd(L*sDOA8c^&HK81|7~VeM~pO11R7DS z{wvw4VGc}IKw$ODngN5UQb0e z{hH?~2;4K6c;4RqeF(h^6>|3ypc@z(3^(kgv%bbCVbZ0R)c{PZOLEXXO!8`^xgcc*~y zv;3|8O;~5NtwNUb+5$xgjPp6&OzOK=64_>@$Hyoo>$E~>u##PL`)ob5wzd*-jO6z7 zH_ydB8cA&f+L6cpbT>6680t88ILZH#vQWVd`> zd@Qkm;q{tDiP;D8d1-N+y}s1!dt#A|cYDxZv40(34DpM*?pIT)*i#B*D>*y&sDD$T zK8-6V-NrjNw3x#%Nj($)9Qf%T-V%KN2ax01%|FPwCAmL#@sC<$ zfK7-t)vu}#pZaVVmt}7E8J!Gfa+1i+V6&97!RHR(Ab;Ax43;`hkcS;yOXkl1E_3PW zU?G!#ZyEV9538*!jR;Z5`TWW6tD}?4w_vv?z!NyQepRV1MfP^EZn6wnQ z^Y~>%E5ICYc_=vi=1(Fi79a@wTnl_vdJ@fe*MD=?^~iG|%jTp10WirwSPqBC zg9$@BJ}(iEkKY$+4!f2WzKX34aqH8V9ErOP(4KCOpCd`5&eQsn6RGwSGS_Q>U{`2o zLIsO_USvjZGZ05Jtsgho$s&h(2*-0LlBGkloEthL)Z$~W?}l$l1DMx?ytkO~``@tK zg#`&3U#UCS&kq#=jVeRSOkP12A!v-G*@RoO`Wxs_g*RBbvvy-tUHP7HyS6Z{Dug;b z?m|^Stz(-4@iODy0PSBlJsyt2rjNC7s)C{~XUrtTB+L(Ouil&7>cN%G?!5htzt2nl zWkc64;hcl7K=Xt`&VEFIX8m;usdWZj|G;Aj`Kl|z4o{47@M z+XI=IP06lfDQXc@lXjzG!)WDYW>iweeR-Rzd&uYao6j=ww}mmmyOf zXYJv!B=iUBcf>Otq4BvO9jjq}^0KO(<1o+&z(3-aZ@_b}wqOn)Z-p8!2X zLmL&!ga@p_mVz7dtyZU>E+3PA<2)P5PWaJTW4*q9$VH`>?vt_k!M=TMwhgr>(jCtj z9p#nt+jTa?oAPB}^)3%9zaLsUk--i$jc2STXKzC6GvE_M?AclO6cQ+v6Jse98<>`_ z<}owj-A+`zE+>rO^H<%+qC-|D-$Qig&+l(aNrqF|e&I=0ZMfwrgzO}_EFf{8)woE; zT~sv7#5KqcnPK$+NWW) zId*AN+h9uG5x|AgO75vJ7@=5`P}XYW<$K`zIVgbER$73%OQaqtj^MxZDq$x#2R zzca|6^S0maFHLHf89w0poIJV-$R1Owd)8LsI*6aZYv^f}{Z4hyxyj7VpRTx;?(!V*Uw;pTphG`O#{I15$?^>1f^mxn6+y!M(^= zuYhgH8Z~VL6#qlH=`;awg}z)4yc)P#w#}2d8{l^Yf&)erqM*xPBwD-g5)2&c+U_21 zsCI7tHcg&Roc(HtE+17)OwJ~?d3Oa!Y604x6z>57cTN1Jolon`vCf`* zlaQ`e4R9Awrq7?5FF^2761NOzqr@gy+faPk42|X8{u44%Szqv*AbvUM7w%fF&IB;wzbcpdD@HJ)Iry6 z@f;I=gQN!Z4QeGOcKSRyeN6OHqcSLlI6qx;I212A(NhOAqfhbjZ zv~2T)v;wnB*Qj+G1Q%K#M1PGqN=w?2A(dz&w%F@GA&qB6AES2K3Jc}LoM~Jnn{c0N zkA&q= zJ8Fif$BC+-OP2wBV^Rtj>LyxQ6pSjG0mHTY?(=pKEM_@j?e%Mx~ARKftsLTb(F5RSQ3w4m;g4i>yeJSj}HR;$04 z3%8&sAoq&`N!BZs2r5hM4SRar{9PN@cE{~U6-H+9btb@ITIjYYhjhB!l(8DaNkHpf ziI@pWvp6q75%7@TU&=eLxq$m^^t{j1{*q-woZGFR=lbw40F z(*A|YG+vzxTM)aAjcMPB7D$Q(n%l{Q^f__UzFYRrGQ?!dCQw@Es;MHuKE-5k$@26& z>-A7I?Wj24c*PUoml&8OKaEDBET<@=0KZ}>mT%H{B!Sj)Ar_I%ftx zJ4u|jCv<-Ngxza=1)*1`p=AOIlXWicxJgbV{9*VlciK+c2&shnB=<(~Fz62I9TQ4P zfq0OhnaCCu1G1f;I_VMb$dvY2SY1w(J^wKHV0HH?prct@9Z)1wTa0eA8cKN?cQH8| z?BjMf!|q4y=h+1K7Q9UiTnsu_d9;NGKd%3U*>vw>L#9K)j`VG=ASy=0r!HS}oym7U z$!1KRW6|)E?H4gYRcFXmt5b)p`FWWY>;DSP;i!ZZ+)!RC6Pd$v%Bq~ zkQ*}&a={)avfCVa+!Ut}vc^RH@m)o?j&W&}7*rq@`mLChlu~KS1c%vlJt&gYneinV zqc~nzOTsH@&oGn-iibSrOrFtgH%S@)xtEZRvpHX84kH-DW^7)EI?57Smbi%Ao`2Yt zNFM93bu926El*+GHHP)hNW44Y{Xv#Bng5b!8Cpiy6Twz0582S4x@|5ri50$8jWr? zSD%jnYP~q&2R^SC=E6<{ri$Su$@Yn_2ZZ~U?}>ac**<-F;T4g&1!X-K*D2EYIZk^8 zh3Prq$1~dhtB@7#$j70-mSyakb+O_N!}T!{_vtq0AnBv zN#zP;>Bo8VaX-!h8yDZdXuxwtBce_GCPo@FVW(S5!#2wY=3I0O?WvQ;57)6I&#p@Y z0_`tCowM)V*eH;3)sOE~`A4SNXW30C*EUk^)15LaINsB{=h&O=m^fFos1T=J{f8Zi>;^_a(Zjfa<+DG=&mus+ zT(I^I*+m8SfXm6Fzd~ttCx`ypqKyv6pV-ioKWe4}gm_IFrNMCFvqjMD|Jz7s1+hDx zAuZX-UR}C)){ZP;jckPcvNF7s>LcZh7jqz_`yy4J2!8d=R z){qWp(%mPZV${Qj%7ZH`HpkaFp3M|~GA#WH8wf>luayaFzR*_}GmaF}Za%FQAI)NfvGsp%klxk%6K^;s4oYTcpw;f(#Wb4d9h936y_htuW274_W?|97k-kfp_*mAG3 zc7np^3B$wf*hy7E0uV7(fH7_#$huCa-xq5lhvJ1gZ@8_G>>o?lfD$|S*zV^&X8C2J z!Q9Qf8{J6@&rSz}x?qTmExU+{5@Y&Dzc{Pj2ujTgritn|=z|m-{`_ zp_KaAMf`)Z{?~^$AgusL`0abGncp{7%b54dmz0}tz5CTCrCUwg{dOX8g8REdC0&-C zu6Q0rSFghKUVy2dv>vAem%Y1>@=wyeb0^yawljf$gAVs(I;C%hF8I&DOt_5li9~et zZ5;oVL13_8&=ph5lq^)Km_c>KG@J78H*ALb>^VlgZJ0#4pFp$tJE%UUu9+Ky6tz0& zgYrRKJpBYG#+uZ}$w&f%LSEuEi~O-sGPycdnV44yCAr~9Dh|Zm!^Ru)V_g5i^G z@#E=>%v-|3?4y&f#4b#M>^%t%|GNvo4gN}~<^6TX=xmPjuJ<7p!sI>yq z?*U_b5?rBmQSRoTZH_%GzoN<&=R0&^Y7{$#(-;zc`VJMjXIkhUAV2S@2WhKP{hj;@ zu!L+*a+3rmd&S`i`;TkV_&0Nl6KI*fedZ-17~EZglz3YOIDYckW)}A!d&l%YhO8 z+&g-Ri(g+@`Sz+ce&nzRx5*u=lWKV4+#)y}J>LrXxNB`SxzoCcF^DYN&Yjlnx3I}^ zu=&zAch`6aN+b6PB%lFGOEK81M_V0Uys+`Y8<^+7Rau~rL9Nfij%C5US<`Qu?fx{4sVsf#NG&AHR79&!Dh&%tQgB8+95xxxDNvc!I>CrQgn zRkg+o+CI5i$n64Tp)w!T(np~)+$z|-47v4jT~<^*EOsu|htf!e*zklgL~*PwVm(^_ z7)B+_G6g66R#4?`mMN4O=zPZL1fI50y`nvO;~t^z<}o6Iv*39|gV!#y_ao(#hZ^>; ztV&oe3R2UmqbmL(zOTLM>^;y>e)^gp9Mcz-u&c_c)-nF`g@vuK=qFj4eZ+2oaxYQP zQWbMfujgNy@r}X|o4kei#nnA?x*Pg~^o8i#!EbHLUKz+|rOX?mzTq+vs|RP_gM9K< zu}!qapVz8U%a9F^(ipI=f5ALfvWl%J23IVI8YgJ`DI#YxF%weU2#%U7sgg$6rVQtF6mmG8#{l zT#0Znw6j5S_Ya?e;#VJX@8Jva-)DaUV6Bvw2uign2-yZ3`H|Xo>nrB}hu>BBPoh3ou@PMM9)_pIeSI^pzOjg&>RTv>G?RtigX8 z$Z(2Yas30a4LbA)JMYvNGFh3_K?ilwgQ>usJJEg9p_z`@r((fD@N{Q?$%Q%)mDUpF zrFy@?f%?Vdbmms#(SDDS=X&Hr^Q~SV`+f5*k3s$QERSfvnI%bn^yd6%IPsv>4kVT7j&4!aWSa7TMa!(HUX4FwAk${Pz$K9N0 z?((xfy{b$B)5{OPDd~%`9Uss&wv1y9*Ewprfg~ecXF?I4Nb}ebe2Kgz0Z7NtxK!07GQSSLm9MwDS}4#G%vPc&Qj`C}a0UkK$MuU!t}W1MsU zhjar8CANKyz7;ghdsG33$#o@ftfQtV`T@r*fxGm*{>%|Ut#B7<9WKi*I+lk3TQa?k z5#wz9tpA9_#xvRW(sn7}+UIw(wzY^m!?B8@kDuB#*8qBAW}-~StW%Sw$wIeRh(EYv z&^Re(b-DXEj~WwIz#HR`Ou}$@PI_9*gy<4jU5BQEqixN=NT-f@>gXZ6CDH(!1Vt6K z&xIi{Y}gtv37_KwF6@1Tbi-9(sk{79_QY3+A&#)ET%_R{vDp^jn~HTK!+sR9MDb;{ zYG-9nU+zegCWe#RVx+DU0SF4z@>~L`7bO-ex=c8#c6mh41&5>?pm!7C9y?`mN zFe1bi3D)iM_xqnU*fjOxQoc&Fi+zFD7l0}^$haY8F+B{5rlOQ9&dtI{(`3;`8HpX+ zs{u{M%wxCluF|qP*13VlNZ+?j>(3^8vQ|&ME8|B&hAK-H)>m9!1x0IBEo}ZOwwyV2 z_BS(_V(zN_9#pqdAJ$>&oPu`1L2ZI0eWCAl^>a;NB;p&Ng^3k1%C-X&Rs-)rdvLwY z9sHv{JchaR9jfFHeu6IuC9YMkmxA8^IJ~`S`f<_hkUw(U@Jm9U7{@*YT*peD(kOrW zIJ0>6t*K!^YM^{<2QYWyjLr0?^WKd9z0txI<(5vUdK!T(KVc~~-^aHYbpDOLqgGqX ziJlJUJIRw+@N!+HQ1MV-s>!;g9V8(^p)i0wx?5oCX;0}_jAvRi53t}IW z__5g%BcToB_~u%Nug~STJaid{Td%Q|=!Zt$)*EcHZ&K_z-p3&@B@~bG(MT{OXX-Vo z$SEe0DcaY=><~(8@9;8q3c2fU(&3%`VG*cKA zc^yG$PBY5u`!skFcTsLu9FEMQsF$L*0wr4!{YXE9Az&b{eOny>>OdS91DApxg@F^$ zt+)&R^t|(#3TWixr+|B6>6zg3WIp)7E#uvRN-N;Dl(U5seEy*RSulY+k*F&|(q$_^ z&C-(&&tO z{#Z=7@Dx2@B+!`S(+I}FZ|~W~eHU^gkA;aPxQ&dn3LRA2QtzX1@nLF2BFPd?iG$Xz z0qzwC{k?CnWI|PA29U7s)*;KJ7L$KG@U=BEd|0sOTuoLc>iHssav8~1ONlg&2s`^n zhx6j6&D-+-dfzI9Y4R849BPDdONpzyZ=_^rB)(B#v>nNtL^n^GwUq9$`7H7RC*3@s zjgJ)K)5oLUi|F<%I$Bw%lMxI7|>WUQIatPA`;f?U#hhS{Y1OL}39Tv(*`J0QyLn%p7pY=NGYv}JexuAT4d6hHUtGs!h zyZ8y?k5r)gwS<@atkG5h0u{Jp*I$01O-AD8QN4A)$+W}ErTu`+EXv{5zs_#T!hVjS z_-8N70iuGyazB-n_-rNp`r5NZ8LQULC&DA8WK1K@&DJ)FfHh1SCQ9c>ZD7SiD3O*e z==cicF&sElt+otEl6|Cx ziVh-6lD=X@-XTB8ZBX1uo)wcJK|0&XV~97U*5MS-yBOsNQiQ%~FBcXzS<7m5ZMs?H zDuje@xCQhrm$iZQ+>~RUUFagCYhzB>&mN4B;V@bVVkvk13UPDz3v=hwlLFR>nRfF; zS4&r-zbr~_ z-$|KkvhMiZhuvT!;q9)t=M+j7{{)xiuycC0CO@%7Q+tM!>DFD$R6b=l;1&~)mXkJ~85(&LuMP8*X4~#$ zAUl@p6YXASQc~m`hJIGFBYyl|qk>}uhbO2b&n8eN>P{Im7L+lFeZr}$M-J8cIs2FZ zFBiUS$Q9)>F_Oe{3HZ6OjG$Cfr~>H*rSz0nkDKF~2c~axhV2KM%0gtECVy@C$f>ds zExJ$mom5PQ2O)6me)_k9NqM*3-)>dTr(PB#bB8a}k8YcPDZOu9XscV=x*hOYo&4;D z%+1Mx(UCod!9EAH?s0RnmGKq<8PgPnxQ%&1?{eT63sWhl<(d0&{RW4WU+XL?!tdE< zLXE$)drEqr1_YS~GQS<9+yX--PZvRF{$}9rjWuKOcrYB1=m@@()Vg?}qWc%3+45Q+ zM1nSCs`tKKNqTr)9=DOV!4-!wvN?K%X1D#v$~xTTK!UFI$9Q-UMUt!$m9F*YEzgr2 zp{TKK5~o3N^x-YQHQynh+nmqVC)a7e;$xhS`<@(LZsN*92BVBXl%5o4?ywu>mpTee zTvn!C<(9{?E?NmAXz2zjN{``Nm|)RIt5Bq;kC7Z;0dh?O&6A9t4~(FAS(EtMtPO{R z-7jKIF9$i$GAex_yI`voA)YaOT}gazN58YO*Qr69k=*To%GmY>Of`wv=w-7B7k+=1 zixM7(pvET56{yp7vV{{XehQ{C>C#bE|9Kltt3)@N7S(3yWpqM;#v6rP8_`CO6G7dm zI6cNyHSyd|fWnUZ_aZ;nn@kU;+}w<|DLt}NNmUE}l(WeQhd>p~P)pEu;;kV}h!U%C z_#2L!A0~Z7#<~nG1ErW-`Aek&R0?g{6Pm(=aov8^sa=^;1=7kSP^X zvTdws{XyrItW?f`Y;$`_3CaV3|Lh73`P8b-n<4&$ml~^Q)q!M`)hh)UN+uTJig%6r0gJ6#TLVHh4T(9gBgwt`vZrCnJ>CRMYzTLs>0$+#2 z$T?Mepjwx4bW{1qRj4PACD&H$91SUpZIwZ^P_xy^T@3s2-f-Lu1ZIWelyxcJ5Q z;3$`#Cpss1cQv{k7#~%C`86#J!*g!xdua-J@dJsbI`!SaJ8&M%%{>l`u$A93HMN3X z%O!+-fI}aDQ(KQQ)^~L+iH7s2dCWLoav#r9M6biwS2Qs~=#&n!-b^X@z@*_5PZ!YRIazIRXG3Q8UO>FS z2aPv`ukyK@GFH7)o@WYB>j+L@TH2Kh@WaSSRi;CxXMB_*-GaT#ixn}O)Wj|Y?g;l} zLaru0RN9Vd|MK}4*)wBZ@b;2@J@L>1pX<1bY1UB%*>5?10_-r$HE(K#>xz;miy`Ur zGyJsaVp}|vFZxW~f0Ch0grH>*a&^96d9wF5K(T&kWj#vtLYI^t!IT znbU0dbMHfp;yL?%gNN_5lxYPqDl<@P;ugBDfaFJ{^iN9VY9{W$t_l`1RSgv|} z*vMsaRO#PV#c|QI(I|^haPqsIHog_|EHfZ zIR)fJ4HE(Nkf+jOlPb8w+x}7(8b=#cc?KBvMENFPGRA&{B!?z#3@R*N9qDvhuvi1$ zepJGr7IH#OvNvr-QSLkRnYqR){RZ^ZL@i0iRWayvP8XLL)qOooZ+q3fQf>^H9+Bgg zQR0#smK)KO*8Lr5kdUY5nHH#$F{+5A9VKKTOn7@Oj$}@5C2@$cxu=@8DBh$Nv+KaN zLr!koe^5*S4vp#`mfZ*++oA8vE_W4}jq0;SGZ!Xa|C@|uQ?|y1ij`^aJs3)=`Q4zB zuHRQjvfw61lnnV!IOiGrCGeevdvnpM<>+EX6dUABSdZ%43q7gn>3yA*w(;&HG4)8NNvYq>>5*3|yuK9*m&Jm!Zoi zV`T33NNTGQI}#wr{@rC5`*WLp&8}8YXp{~ri}vxV=bLJam*rjpZZ&;Te+s!#T&+3& zbLPbqmdr&&BmRrDmBNgDO>JS4qv}|6C3fgJKi#RHlTYNaa`{M~AdDszL0L2XsXG%U zajPWXF=r8fPNaU{#S}%|jfPFBsCwDr(_XyhksQaSFUlqI<#cbCkPF2D$oj1*4ecvQH{tj*$U4Od{5)*)UX9Mo-)u5mn?~d!d{60cw@2 zS5m{T!m#V*tfZ`Z4L>Yt@3^O@ype1CAv8UA^6In>T43lT&Giwun+Ki*&URLOoYhZ zslc2d*R}pF25YV|oaf1apil5hApH2|FJth*J-dgJ#J^w}=Fz|EcPo51;L}M|j7=|f zE;qmD6eKt6J{FMDKK4eM>ISX#!B3Fhnb`I^xfrLM@y7}FXB35xm*!tKD=%^ReusJ& zq%aH@tKG?C$wGe7)&JHjwq0C6Rh>p`<9wAnyf2KT+uhwHqe)Tlq!fU<@?!-u*&P@KM*1dQ$U{cD(AY&QWD={ zyTTjf2Dg7M^_QW|Qv#OJ2swN;7dyT+U37D;d!N2bsesluwLppU3MF>AtHxZWY>{Q| z|Dt8O=h%5R=l2#YRhKU;#6_nj=GZmF@@yoBEMqs6AzzvPBzOMoY~aPIV^zo?PD-xR zoOI2TW9}PUNL`5@r_{IfCfb#dwPh~%2!AN%#gl4eVo@_H&@Kig!BcZS~%?ub$ts+4%Tnb8tI%Dgt@L^ zF2sm2C#*z~B2EXImfvRCqF@S(oQWW(mc@`;#ND3LL=7{ndaX>NN%DporS84*?la4- z!GqLL_b96{*-toJb`ti@UvOnRxPd>6?~Ealw~M9W?j8|+h!e-nFp^|TmYAt+e6QeY zyAcrUXLhUmQEiU@K~#A{AcEA+5j+ACN+|b{@6_2*J!H9KA43iJAa)XlolWQ~@c6#( zXxU<$W%jvO<-8~Ul=+J-V)N1m6s9d3cLUTtJe>Ok11fg?uzdlx512>Ff@FwMeOEry zqh(ap>spY(kje9b6sgx9`J8et$bQ=ejAveg)@ZncD3!J;1S)vlPn=;-?Q3WS;euU8 zIuylbPNt9wFyhAw7WGp$@|pNG)+OQe%-$olOERI5p*Q=O*SOHBen&?($37Xgfs~oD z=;}^#LXv(v=o;Qlu2@A%+#$aha-52zT{6*1$MeOe%fc&icKYhem=cj(tR0#v?|P2a z-e$65F}`=V0J)Ds(iT23dSc5N@6I!S{LdhtDm0eBQT_wU+QH=|wkjyWKhF|gmrTei zVn}1xaS1BRz<#cJ@;8TC;aS1Ycv)f;LCZw90!9_`1cDOO3G9-Up2br3{x4zn3)*BP zPHlNH0==VFbQ({1WjomXsw0zD*BdTJZ-Kv?B^g)QN_sixh@Nk3`}Q1RTNaDUE84Hj z!VHkpz%Su)JJlN~!-TV=WW~epzQYxbm?p5wJKKl;_5H4S-_Ab*za6;sB_{6w7s@R~Z$jo_=dSP5 zPp+(Puas{+Oe+E3CR4X2r$Zg4LXQyacj{LT7Y4ObT8XmAy!%6{^BTkbu7R8W5am|; zv!Q<^tmr?=o{4k^5y-c+*tFL$vCMY<6=&yFajO4$CTbGLHZIzzZDA+IwphtJb|6Mn ze|TNcx%0e0*oO`u%jX#6!l8RHq=hD0)CQdGGSEenPU68Sj$r|HV3K|G32-HtQ0!Yl zUp5j`nTO6LbZ0S(9MwEBr^P_US16eX=#=&4u%=M<&{IUo$U=Xv&b$AQ*MdBS{Qr9` zf9{iWa{ZA7Y1tWx;c?sLWUF3#03~cg#zgmhsLG3_tlImlpiS;s&Sux7U8%)-XI_lY zZDEBIW`m{-IT-prEXljSxL&-iKCB zr~?|h<`zn1kt*egfz|;O4oO&X?5uOoxe1dccDW??eB5RK zo%0gYg{HK{s$$WoDZTOPt|1d9HHrjto;cTs+F19;`Aj%2I`YiTnfJHwZdSqsgln*F z$|p7&nO63)hwA;3LeW|~E7@Peyy@x-+S6a}_MMEMh5Lma}_5R8dn@G|I|V{Z$%68!TtR|KeNpcz-o$6&I$M>`Io3 z-(7s8k8SUSkSD-n249Z^MQrg4xhkz2hDLIy%#r%PQ4e<+($02ID=}>6p13mx{0}9= zQs4VVk4FuQZ)XqT&b~IirVWh3sQj+(mnB<}goWLl&edJovGLt!VCR=uBuX4}pKUz_ zkRDGhXY;x%r8;a%lz$99>juO2cupQDXUam^G=9k^OhGCqYH*D9OAe z8PXW+fqgE)UEDdQ-L$m=@E}hbGCAx0&*bdzZNTwcZw<)lLM|^|L61~Qj|f-m%L!IX z#k_$2BUOshKsRYMR^$+Yb&Ukew-9@IoRI6^E-PByE6HZz*|&`*>2II{tR!;r?HQ$$ zWqCL#xUuxQD$^(9A|@oi-RLgY2_t%vWa_tJ^Kz{8|K6p}gA!q5toEBI1nil#(*)9= zizONLLOZm^LMq6G%Ji02?U13Ru9!)cBj>}T?SSKbp6uf#OmuaoDuw?$I9s+NETF`Z z8-t^}RUm{`rWVaHkgx@bktl8nLirxk$n=Ae%_Kr(E%h*>m|jSStF6>x;%tOyb>@=A|fPkH#qai{?x%tg*JR5}nvk~)9*Jqw0M;v9L5Pe}w{c=(owvurJ7wx+Eb@N&F>d!K|v!A%Z{~_zFgW7DnZjT3d zijyG43zSmay)9C#KyfQjio3hJ6?Z67tVnTpw<5*eJ$R6F^E~hS&H2vxmm!nPI-lk|-8*&*()%J1#KFKSppIu}F3 zxrw1_(c7Ss8{^F`^-9h_%JtR!>OYip$Kw%0h4M1*hn|Mw2QyUGoYb_j4AqS9&X&Sq znWRAne2V&m)>DgnklAWa1bt}dA_^30&_LBHp@JTNyX{$sfS*z7%>(mm+LY0%iooJu zLH0_sm(vPSP(4nLS$Ma|7=GehqMNomuPu!u#0xJ@!(qG%xm8H`Ct&JQ{1MGV_ zl(=Z^imWDjp12oXT&31|tqFMz15TLlfcZuv4&$__vnjgCMMsse?ivL_D$GHAu2wArW&=*TZ6nr__Xj+ z4(bMaagKB`BamCbo&J}-x9!+-f$z>xFc^@iuEU0oAZD{=7L50nEf&|E!eNdT4`V#& z&La4{#>S`IQ*pXFBK6Vlc`lR9p@+{_OI5dd!ZFN>6z(j*2=#pZ` zypbTYjaO~C12?uW=~7xO{Mxx4v3L^mu=H!-m&A0M54}IcfFa^lq1djq=ieNX;hLPn zp#8RaYgaE`m5Y{sZLzt2FB9r!wF&7AxAb`?GAXV>AlB`d{e)fIG z%ck30FLZXZv9L9?HM_)W#nlEKZTjsFZgzLyXwoCMGEK44_6~rXd>{MHQRNbAwbR~w zu8OR=e2tIP&bQ$#=;-{p=rOx4{9dvdRpjNP+%-ju~N#$@fkue2G$kG3~rN%;`vOPi0KXd3h}QELNPcc|ppOU0M5 zO>hM~o?mfCK)**Ji&tuz;M~unlIl`&`1x4h5fh<5v(&Qmh~nFr=FB&qJ=kN3-`Cab zER!A1S2#3Y+)*g8d1!i#4nXA8E@mrJ-doG79!Fvu%8K~iZ~iq;gOtkOAN|J;|H+*0 zYS(yBE@&I957_ODq8XqLYw!G=Nb%kjmW9}buc0tuNa|5DOn2WK9nme)t$TE(?4MgM zW?+m*sEmB-lR5Z%MH~{KikxbPi*p)9S{W~U+uWOlcnjYeNWFf5j4hu#5x;)vZgigXVSZagOrS(~s2&#_hC;cQFfvuZA@#Xi% zpZqKO2mEW#QEABkO#)AX7el;JV|?o?B(u){+NNBXPNQJap>*?P$Y*@NJ(2w>Chu4B zP5hm`eH<+$yGt;xTjpzm(Mu8qnhmq=Zg}%_o_yK%%PE;CmtSo{pUtGB{Ibiy9x_qw z7cT+(s$>>SmlorkDfzqg^iQ7vc)@|d51kV)`p1|4!JE~f^m_z1+tevf)gC2u;(vM` zVj{u=LOtdU4GDt39c1TNR29EAlTdclpZ8SZTr!D{W9@y4%L81Kt%e4?3w`mR7?}E{ zjcw_5QVTfT&n}Y*{?40xY!skfvVTbmVT4%0(M^K}5<-T(GismIGPjB2lHBQ>Q1~l;X6WKX&KICtKcgNpC6li@UFgf&t%-?%LhG-|n3M*Rnde z(c*pVI&JYgspS|D$(mNg%9TuGb(frp-bTcmb-&cCo@YYhC9!+Ltnhbo$(U!NP*GCV zj#5nJ!m2>i6UcgYT@v8+2jo4B-u)jj%gy)>q)yqa6!!i}+L52R~Q%YR40)qjgO z+(KUuw&L`GlXV=`Vn!K)w?#2S&v|_#4d4p#_Z`^ozzuq&gM*`HnI>kQj zbS!Ma4DQdzcK&3uz#}7@-FNtGf~w1m1UBDNpPc@A>vZsyIwh_bdUShbcx!>6I-5!~ zFz5v2u{k-!W+97Ff2C(F&U~?C;KzC4dVn<6-z*e6lY zx}IHeej-4E``;gtu6}?4skX5`9|S+JXsY~9 z<1B$}Ejwm>Rztl1QX(Y=!D)TXMMy~_ks*ZUC(hDK03AI1Ox!6D{>_f?Pr9=KdFV@m ze5Sc@t8@S(QU#)8ma12S_X~tT^N3v2;5C%5x0lR`_XY@`m?#n4tiK5Fpr@e6h3eoDQ%?oiqKV4P4D~%|+TLbZ>*&*)%eegb$o_e?qCN zH?Z>2-y95va`2amL)k{<+*QdWWAAcx&4TQ~Q?Bvi--vZ$oU^`e-l9S;kl$*wAD_P* zj@rmS7>FuZy>SH($B2m&5bdzs?cWJ(>IZ2TA2R69LL|tJ$`5cTXk6e3m^X4}s+$#i zVImHA$}#CKj9q8aAK~S^;-P1!!NRV9AQh^W z*PiZgoq8KzOLx$o)0N3x!6Bg6TdT`b%>qW&51`Tg59fd}U*z5skhX4lPX6k;_T4T9 z$vmE>9EreUMdpRiK#FY|^LCs6sA)_BHeEp0r~@d{t~keDor}o`tyF^lTSVV(fQo~b z-h2f#7~RY7@Mn^k4Y=87zj5H!TOm~M;7T#qkG#gQd8`_<#1DllZ~GBqN^GE1U)-*Ak>d{AF za7P(K2rn9>v~5hHJ!IR_fh|Q1Thf(bLhO}Fp_X{SDJZM*aiD7a@_Q^%Plhly`=eN4TnN6vRj|l0HRO@hf^4nWp2;s~yEB)+uU1@T_q} zJZ0pqXzXmH^nHONtJ%Nf79?D@PJHrF;n-SLjjlFO|)CvA+s%d#c3)N(^1c-FC{QY&$Ubvk|97m1Wm8b{XlDJnqRq@OC)^gNPuR0 z=hN)i1D?sHgf*I^NcA6-B=*a)j3k4AxuU(xg~GWmGb~?zgLOxd9nBdmORV38Ynh5B z7r2y`>0$_RimxPOsTctl_~Bdk(O@WjPj)k8?brL`Iu7<}z4bUWpLmmqa zqWO@V(D#WlTz0~?W!p+UcOJ&UcoguUt{$V0p{(CnyggE|JAx z4s&{Pl)NKJn_2G14X+;9U*;dQJ)G5j^WM%9%v-eK1_YQ%)VIjZ&(B4WKWiEWbr<5faygvsKTLAK^-?0e_!p?El0CWRI+2$U5p=57|TXpo7v*i?@pz8jx&Le>L$Ck zdj)oE_7p|YUqy{iq%Ys+F^G3vF+n|uVss?{r&Juc+U{*4y{B2iQTY6n4^sx`Bbq@% z^$m5!Yhbx!vSr{Nd3(00u0k@vixC4WLur3NaWoV*iH5y)fT9)O>3-Xh*wLD?a1$Qi zF~|#3`fULpg=F{+DG&$jVx+r?--bMZB(U(Vi3H@B>+=oE&P@6A3JDXi736QSTPaU5 zg%F(jMmTF^T;r4BimL4#wL+FFP9r+m&? z;EnP14*+d9l)U96W)?}7(dg&X1b4WyUB?TB5R!7;`d&YN#&)^`;#)vM>s}!ACqczk z;k`t>PTO}JSQ>FrC%TBO9zYiQ@Cs+G$%@mngJqges~ER_nWL}5R;jELAYJ}tBGgJ# z)jklAi}T@(!$Q9b74&um7hj4=#Sqd?sq4yx@|*=E$uYnI9{7Ke8st?LjJVuWJ|}u3 zO(G>x9}cLh2>o7oJ9ta4ShO)s{9fMn)YK%YUY3Oubi9>aTA#{KSz9%+%MO1S{PUFY z>`hn9;R3We^i&2kj{3Q_aKzlWWa!2y2EUr~OWuEiofA`bR zq@`WSvv}}>_a6H3o6tx$sgPDzzT>7o;^povzujv@QHF3?Rz-Jj)l{L6lXc>6-UE>* z8(BC9@v8qusa~2){|7@icwHEqar-{+AmLu6>ob=|brhE63}r^m2_#|KRyzUo5%M=0 z1>;n?Zq>j$XGlT~H#u#wZY=aQh{k(e=s9W^0~~Ej=foRYUT^iWzJ){WHQ96-AVO|Z zv;yJwujr6BVO8?(fy?^FTt>_)X#Hoh9mw-Y^UbBuWh-S_;%JBLj5G#RTrvDb?FqNR z8M|Zc$GQrAA9ZAZVStoT^FxI__`k|A^4Wvn&WA0a zVGR|HX~9KoiJXLC16GKwJ zKl_1t&##J}BuLTxwUjE_`LDPPm(}JBzwB&-ipTaQnQH@JRU9&Y1s!Uq+^}!3(mOwj z))tWtKA&%aw0d1{lkG^zLN<8H##A^7QVw0m?YDj|IcO`5qW-vPB5>(8x84bVbAELiOa+7@y;>grNeaR5w?ET2M9#j0>Ogw7rpJ9mjX>Ia=gH9h zi9tAX=f7^S;FkB!ITu?iJJEVU;Hy23)*FGCN7~r5-Qj%pp_PPXTDj+dcP8w3arq4F zIdoM5JDFd;4=emFFs8Tcb=LMYyHN3H?fv_iNW{PzeES?yA}{!7J^-A3X$&d%i;^rR ze@w5l6}t=FaiOhM{$RXQ*Iq>@nBBVs9F8jtRv`#8}=^vTN5w z&Hv&#z@2I41#;Vt0TEyicU!{;Mz<@Hat1Q2JtJ8!Yb>rw_?j4`XT%jkw+)C?T+J$y z*tkJL4Pn=j6}-CHY%E$C=9aVnF21!y_`()Df7hz010+xFn$2})+`ve{c}ky1O)`om z#Qf|6jLJ2YA;t9!(B{pK^G=o>V?-;8Wu`n|^W-^Pa6vXU>(Se=O~J3{{eHWKs3Get z49qyhG5=;FyHqZPh?VZ}t`wlYE~00KHO;7NQj<;PkE$mn5s)_Yr7)K{sH0@cB5l2V z790lpvZ3k43Y*jOUgTe0(%+(BzJZ&+pc*pG8} z<||?>+{T>a>ci++^GgdAeiTRn$#_mt>j5Q4GV zFNu`2VtpjtXXcq@OCRrO4eLiu#;Bmn^4IY(4&s)OP?J;@he=Ptkra=n8soWdA_7(xO z4&N~(d;&NsFAIdN`{7(thio`7_J#q6zaitniwUG$Yyl7DvU>o;GeD48Br_9vNJ0w=QvXlJkz&i*(*$Aw06E42;P{Y8ao?Rd z7Xur!Zn}qv@82?~neg_4{KMisaXyN{McSAJIPnxM^&x@tuMnGAzj#O>FWQ zG>$7;8wa>+9=K@BdOKC9$G?k9oL@12RC1WBQswb!j7o=#dF2~Yq3H|mNAu-zj}(c0 z!2CW)o-gob?;*&O{(N9>N{~VPTUEM`c4`L#X?x2cuxD-0T##5}yw1x-XcD`%`aZj1mlNs>vhf(&(i{`2G zt~UdrVShpq!^&ah5k2`7c}>2>fXJ3_LBO#$=KRYZ@(+cN`>u1bJA9C`ngpR52uC(% zU@X{Eq?RftrM&naqhB!1cdL5H5n!2hOoIiYS^MV zA(K`%d7!=VaYvEK#-Q3c*!z;oNWl=%O)bGW0)cjkz=^TdE6%D&W89`IyM3hoDzpdo zFpIkO1~4s3M;~oe$s(>XtQ9=@7M&z7ZlNwF3}$cJ_N&&?}vcBq`$a82Ne z>n}SI$?DJnXcFz0H@6G`@B9FJjZ+)W=1U=JdR)o+DC^^B{oHbycRzuo>ECMvm)T;GOx8O4d zk45V!cFJ~={zl;uk#pL~Qu?O|>pi1xT|u7$TC7pI78PJX(cp!s@n+hm@I+vSydn#g zn&tC>ySw*C*f9Cr9j$Z&`1@AB6^E=_{gzv}Gy1h!y*3590=2 zo+776>_GTP*Vx9fUG-QG??qx<->N?o@AcmL;5KkfZu2Hb3-x^>5>up+QOe>mdh+dk z9!8p(uYMImOBo>)v35Ul%}voBOIIhUM9~e=pJcy{S_E*`5Gc`5WsGK^S>ycpq~fVq zd8`wv{Z?XKIdN7WzupEo&-enjcxH+YpI27+0v>QZP6xE7wfxA44(y*6VxD5C)62U~ za~0{vc;Czv9cZC?G~RMhgvGWp-R;)F@-P7V3Eeg~8B67Tm-`jJ4XuQpT=kZnn5i+Q_A2c?}t7wR^xylvk>MxAk?tWXHc8im7{ zDE>!^+k3z3z!-NkcZ~+mnJX52Bw8Tz1l77ijM4n+Qe&Wi$e@P=C!aAS{wvnbyQkG} zh>0$@Iv>7s@7f);Q&w5uNDN`B#O+2ROVI0eYwZ@vGpDk09Pp1l{f~bv!s~c{`Uoa+ z_UmXvQRrP`k|i-)0z33a)7Y7bi`5h`$!w<#X{BQaVlZ{yy9;$Z9$i04p8Bl?8y~J@ zyu97_&k8l2F{7U2-Dm&N(w5nKH{io&l!be{Kd|7rwEG6gqy>*zyrmj1jO9!lt|qM9 zGBx_}-*n`qf9)eEueXlySG&BnnlN?Tzuo34PJjRK6K*&c^)>R~E^li80=yjpk_`%8 z%S$eCv|#QC3NoENlo9)^fe)P+p?})(@C^4sq(0%Lboe zyKF1j8(}L7TsoRO3GXz_f0v(WyWz$f;(~|cg|%c(c}0sN%?u&bXYUddJ3uOU>?MG4 zx=O;P3hLV*!#xMsYRWL7V&}Ua7gL+v@fj-)GnvQt_p=iLmy?Bpig&iLhc4rT>s3)~b~xwsDyy z2^zh@I<=#0PYt6Cd+yC#=zJPil@iV`^VP6_h;FyWx0W+hq|LidV>{0(ahIO(u-QXlicnpdJL2rXdmtr{=zIwG^=0~h_ap=Y9AE;k`3b5Jz@b`KK{-4Rk z)o+1%?OD_t5+F{MDR*eC-~50n8f5-AB81r%7g*`QCCm^2AHu<|yu9*KXy)mtN;|nbF@C4aB4}e&)#E)t6l?Wgs%y zdDH`x81e`CI|6O`Oy;!Re7-Nw8`f%BZ}Gv!6AIUM$j`nGwDT<=NjITUklhJg?_Rzz z?(edVZrfISPTx(Dv`0hlzaAF8oX@_n_vprdGU2w>Mqj^;L!cMZDa-0_8t|LDt?N}W z`C;=`_Y3)q78)<>a}2WqQ+lVd-P%OrOtzX>xc;>))~ZsXQjUkd`VHZ&11FlyBfUtb z1bYF_PN}kQujoJ6-8nrUG9>h2R{|g@S#U!d2ci2w3Giu#-KEBX!#p2L*cQp3YbM>g`A$VZbdi(u8@Ma@vtwHM9OIC^a zOcT?|(Th-Qdy!QRWGK6wD0_iQEd_g#LoY()Krccahj(ZJUQyhpy;;_aBb*8o4;P;h zLs^DtEG>sz1U2q$AGM;_xQxE+!y@{t=ct#Gp&x|PA`F!pl`ENGo72Sgg}YzF*bgsU zTx5#;zZgfi?)@dv6sn5|<6z!IZ^$<=lANJzb!g!reLB~Nx1|=S*JPQ5`9;9F2_F3cL3@uO?ot7op-P=r3E^X6q149z|4-npi_-I_MZ! zAQIym^t2iUb0m>ulowQE>!@nD(saQJ3i?>KvkAWIdE_&=WB#0+&pd9G{atkLerV!g_Rw^nS2kbAgxH*YGLMW7((#0fG~`fj<(c!phQQ#d{e zb%6SQzb?mAPA_i@d@bC@efJpD;1O!54zK{#2EF`1oc&I9@3+%33TQwyQdYc0ls=fR zXf?);fs0%QxF!_B;J2AgycxYdkA0UJDl9SwvNA7Qs`!H5j_t>E#n0UG7?T1eZ6tR% z_vi22dXemiT-H>-QcyfH=a@mFFLV~7cexLxx4n!gQyq57%Z)U%f><)7sPZeYl01Xe z-lYU_CWO8b_=jKX{N>EKcjQcqEcwouprR4Ch_T_R7 zmTq1$tX>#ay4ok1zs|-wHxyRj{j^HDy@g4Fo6Hl(X=FJ2$ z9y)j@V5y#sAnnn;OG?=@{R_kdU|7c07EZ8)rgk6EndPJXkU zr`JAJqP4(d+sDPV%1;ELNH)GhAx<-|{NwDzX#8cLRu1kzX0>EkZ!1ydymM!*SKgS= zJ26h7ztBb~XSe50`W<587w|U105@dekPQn_IA}uj_Sz#3QRh%*V;v;Voz!|NjbI<& ztoy-JM)Vx#4r_+2AtbeMGIYSMZO#9jxgrIjfPAXP$|3j0FAxUGpplemHWul;z7~+R zXcLpzSG|7r1U|Qx*Z8yuqBM`9GsrO}P$%;GOL;q~%N@#F&+tRy$$-Onr&s)+D=jH} z2(=;qj5ABpBDm`7jgf$HrZq6mv`zYFoH^t(t(6eWNiF2o7IZCi9?Cr$<&y_oy#O^2 z0_}l)>5o}q76S=ws|}vRlYWr!5&l#REraDB6E}EsdQdRd01^pE6L$vBE=IJQoL)aM zHg9fu`P(v1>Z26H`@qrwr4;QOcR}sI?h&B0uIXH0{bZ=A;K}E|VUY7TY%k=xwOK?rh>nBMIR1>nEDF9Ua1$FJ+N?!#N;Lzt~+{;PoDe} zpKSjOvJ!E&g8kQPR_zab4P-;U&lY>getX)A*V9#*_m1yT;&sB)R$#zw5BTBo=uO*` z3zCR~3@8eFv7NHspp_&GV3Y~*OymyqeXj~q_6L2>y!csuGaxU-;d)~QY%Lg3G*l9O zG&aZ({ujTh2oQr)*r{%)B_5v-fzIOd0iKRt^*kOh zN}=X$E4;d3yL&>y`=y~;aEn~=z3?lRO|7@gkk?%La;7!-!po-9z+M~#a@08PO2Iw` zw+9Ya!;-B?kE-m`AO*Z9MZ+8eD}h?55w>jWq+@jwqac1H z|CT8>+hn0J)YOm$t(#eVXBF;qoU{dVgD(qLKXA-bLUt)eEDB< zhFvpD*uOlniacPmMm_-Mt)F-arEUV2%Aon2utaphsV1v4jm0;x-vX9){LB-aRY}@# zO$r&fyUFnsbLhc85<1^}zA2?JkVcgECk0%is%=23T1MSgm!+B+)zhk~jOTV5iumIr zv+yC$;?5AoTay7-=2wby%}vMR9gkd6YJX|4C`NGP_^9;X2F=Qm^OJUBQ1$S(>gCYH zF;D#1x$GKH=cR)a+?EeI{nCrj6evZc{Dr45#u(s-T~*V10Az_oZ%)A2mgjqJ1O_~q z^r>T{qw4V~1FH_}A;x-HxP(r_fn1jmflmT~r&vMEBkiLl*cB=Q$ZrDtDY94xUNt|% z8HJWlfDZ0F{q}mOT?I4X4cdE0=?_vGvLi`0^;zMJo;bj=gFz9}7i>AMP&!aFrv*tOPLyn*WPfl8Z!`v1J(&!sQ`vA`u8 zmLK%0^ISe$BuQ_V>=T?%Z87GJ)%(A>V|Yvx_*BzDvK`kGIIaJmLoG=nDfaw-Gr7_3 zqS7QGDlCB(fkUn9UY(5f)rOv{)uhek)Et}QBx7x@!p$)y!XtCXiuBfpCz?^IQP%~F zGR*f8ik9D8xQj;_inkgSiizQ>(s9|IbHx5Z&ul7l6`(W#NET5u6Vko^m@g8xPsA=yM0j$dW19Zz+&$=g+yCZh0eCxojvUu6&;}n3)gVSr8G72kTr%l+>j=7IY<+T^ z2!#Yn{1{`>K|5V2@%J+?Mhg)4{pdh|u>+*97=%|H=W1^A`@!xvCLZf^dO1*(4C@Y`r5<&f@%@NY){4^~v<)$gpmx9G3kCOy#~!x$wrw z{o5F?UQNEd*0e8%8$7Wp6C}q@sXn*p)dfu=*%r-KiL*8}8{^0NkoEVFR8h4Uvo$}| zw_;_lVwnFbiJ==QQ@{M(r?79_OEo62aa*Mc+o-#;_B`|)b$vWBjvL^cxSw6Nc$fkH z)G`K5-|;+8p40TCp)}l;z%^h#+o>=OK_cC2M!85Sof{Ut(7U;1w`TWP>nZct+rFID zUC?zuYQ^~_;JqH|zObPv_-m={X`k&1L7TFk|MYRUoz{*WrlKfR{SYZ~#ZY%%aPxGK zXbmihJ8{1Q)S+nR3g&X`W*$mLVBbMM!?C6Jg&rQ0Uewc3g@-$AtL?JqD6Q?R?3zA@ zWq;cDC*p}FXqgB2*Rw1)ueQ+Rj;&Su1;1ImR0ff2oIV#(KkdpJKX*q)sCnTph&V&LmP1|jdXSWp(AbxoPqr(_VOSF! zoa1(@C=FSbqBdSPAfkRP?uc!s*q9f_sF_TnaB1d6o}O=9XFSmhH}Ka~2UoSdr&auN zC#f6S%s;VQcdyZDlcWb9FmQi+K=74dSX2DTt8J!eVt;$_5OJ77_q>+o`RMkxl~W1 z!cWnr%r}HO<G2w~`dY(BWzHc}4Z~MGHj2zBThp>bp`LwvFV>mgtLmqMg zVIE3dl`fxz2`>*^$F`PMa`6kk!QWOPD==%7>NojH`Px?V2_GcM8u?K zaEzs@)!3z;sxz16S zD93S;_At@b+d}eyE}aZ^xr?&Z;2j4S$^_6j`{qce4!8Rar-^nkg&q? ziV`)IkTtEvLf90C4x7otuzV-)iWExk2xyd4hB+g-<~`j{n*00KO8@mIia)ZnrMJ3w7Tv-q@m^m~VK)j>}QMJ9MumJQW2Kox>-Zmz#w&>4-%`CL_Asit=p&i^FTK?AZK9)U zgCG#d7e|Qk=N)LwYd*3cU`^oHrzZ`@*dZ*fPHjRyIHTIlrHd^IWlMNh!oQnsh37#k zjn+lNL$xA9d9|LpEr_Co8X~gtNN3XGhu8;914!a&m(G$b!=oL;cz@kU*qIfF!fbGl z29rnjFC}+NB-kC2mOi<|Sy6r@HpUV;?*hOq{#g`3c6^)%LFc1We=z&7rZV-$Q#>{h zFt6!0;!pcy0HX|@wKpAK`Cqgo+>D-c5KL)?elFNw&?3_5g^2!%mhTm3<@^nC*ZTTJ#L93mB3e*k zZx+^@@^$ul2}gek*fQbHT_k4krgw7G4xSjY9>FU#iQt>ApI@n zuvjcF(X@Y*H*Y!I0S5Xxcq~{0o>pFDLS|7<_LFwShZBoeLAPmtux2o4N27oJ3H|=l zKV*PQTY-#Np%d2b{aP2{%UP1EejY_!)Jl(pxpVB|acUi506TWDeTlWU8x08I%@cfm zW?qeuIPNh{E@>7+HZ%rAW9Qex`l`B@yl@9D4Jt|{k$SBsv9vGzqCH#t?uRAcN5PtQ z-O9|9?K>hnSK6Hy)H#qPN3353YOPfP&b=e+3mHz#j>QXGYghR#XbFExtd_Z>)& z)U1yPl8Wxq4dV-D?=ImXVRs#&F*7@Da6UQGCI>W;4WVdsnS0ePzZzmi5#JBLK^u+< zTInG#T97dUc0&$efF)gBX@-C82XG*!?zq5xK8-!OX&>-7b>Na2=O1_wmPSU%er-Lu zZE^kKh60h0AwP0v-h8nZGO_NVuS1M>c%BO38lW&2$|F5MjSATfMbt0ww`h=x4qumW zRE@fnc+p=6+V%-SbcLW6dc$R=6C1QYD56TY(vGE#g5uVY3fS0)V``D$KPK~-LmlsT zY3UdhWTU<}=0HeZkLBwp6M=l&J|c=l0u=^$Yqc? z;dB?JIx{Y7`0UYI^ULZ8QOFNvl!J#7S!zj?H++o`&j*p|Ch^8oM7NdeoL9}|2WW}s zp;ao6M&&W*&iVOw(pT)!uW#P1$(Q4@G2$2I?(?dALRnhF*v{^kb-!n}Y4tTi_)Gjl z%rddp_uPDL=ouo@;?JUD*M2EKs5>N6PqXVqbEEGu!TuF!`d;u|`Ge zbxv!vW9g3kXmoiRd(DfPv(MeTF*8_NN~LT&t3M{}LD^p@CDHS2d3h+uiw#5v`)d|b z?=U5k2erBT8&7t5?kG&O*t@Mg%v~5`rDN^R2oF0x65WC?XT_paj&!x*oNxZH`b+G{ zQGWLMHCF8-1I!`v>|<8*T_bUH7o4+yI=GUN%U@lS`wmr}P7-w@A^VyaagZ*~IG+); zj(e-%-f?fXU&FLJ4h|mL%}ueuHT7+QPH-_KpU=pSMjch&41EmESeDn}8%>p?tVj!! zm?czVm+tmDhy8kJ#W2`Hj+VCA$B*uszPSYl4y>Mf(K25WFZD~)e}goGsg9dAHq65@ z^33XJ{pAbG{9lB$OU!#5xk;T%60D=4TyQSFK7oiLk*+mo8d?%}*RbMb3-nj69Fy3n9dV8n=e@OQ-LSuK zefs{o$ED8>!~Z_`M#--AZ4yy4!i={<6FbJm?flo76uDA1F#=ufxkVC<4d@w^M5VVT6fyP+#0hb-Q#ryq&uDzR&XE@OP$rd+>d4J-l{3CQEw3T!=|LhIzT^V++u zwK>S_7C>7uY}>+ZeZIhzyBojK(MFW zANkb(LSGszB*7zAPX$DH@PQ=~GRar7u=Hf8SC>e{9*8z4iy=hdPl4V~y(%P-5(MY-Sj@Gs zJ=ILl#_kacg7S zuqfp^Y9Tywm2&G7k7$@DI%;10Xq%+h9&FB4E)eE7W1n%7%_q2jCYRz#8EqR7(1V2) z26lMi(DKQuoz-|dvx}0GMHMy#<2DuexrD~xz}B`UC~bQ3)ucxw=mmWvNZw(EukD9u zju8S+3w1_J7PA`#>@nZ-bZqA(T|UyS&y*r`37Kno>|xU%h{s&@+z76ywMBwWc-6kNRQ~y1;?QTA(XG0zp3tMO#WL$eyyIIBAN{P2*=CtDGHrmSe1*@ zxG_iSP|}Xy&}nLo<1MBvsjY{FZ8e(SM^NB~?bO3fMrDYbtxx_*5P$_V?wf|r%;Mqo z-h~ErVfiTe3+2nWh?K5L@#TPRK&yKrwmq>M(fR{LNck4c<=G#E^2Cc^6e&^-t!>of zf?`q16JqdD+Q&03UBp1K($^!tLgpP{dtKPlI_qPnAi2(m&RL$csf&2|sKP|+b!~{e zc3&C1vSLjj{AqTn8y(D=Vt?N)s2a%AAkO;|DH*S3MIZ`B1&j1NhGAuHKDMvLh|{|Z zHJzgQOur@nWPFx=Q2)7g*J%koTMq{?#d=?ic`+|WekL&vC`vZPPo{RfMUJ{Ij6rmS zF)r$XKeii9zjOu35H45Jt{gtbogNiy3#zo6Ne$Y+QkwIo&px%g19fsxsEPkLZo&pS zIH8P8M^Qr>i@!TVj#QEw4Jv%%n%V zR5j;g(5Vrg4C~wg|FFD$e);k6Q~kM2WzZ}IqIt>=JYC$ahFFT zAi-NM!o9y@uGOkCr9`eW_WBKu5inWB#0>Da+3)Xx7V4cA+w{Ms%l|r1uA320(txFz zH;O`SE5nbFE}Z-U`%~d)n#1@B7n?({!v}~bM;DYN-;M%2XLco4R)yrlSR;y*`ECgG zC+ARH`;5kKc+uqz21QFmit$0wmh_D|R)y)S-6(kV=^q0}{2FjO;oj|mal;^6;U;_xYM&HGktqP9#JeOx+Px^IA_^Z9t*7!H0s5L7Hv( z_K_dKzSE=iWnPZGdROP%hrXPvkx-XpSqZ(B9b+dB zu!kp|`}P(?WBspMyx%Pb1oLJO{ez zXF?l^vp@9kh0LqNTApp*cXnu%IzX6Uc<1(EQSnh(e)F%XNQ37VkJl2tqs`TsqKBRP zZo@Mn?}?y=9ba)$pxKL%LA3|H^EOA5>h+7dtt~i;FAnk~6jr^iyuu(F&QG&9?@@7a zy_PRLMyEWpJys1uA0~8@z9Lyjk24CCPlngZO<4r9Cl^X)Eb-Nw%NZc(J03LEr()q2 zm;4ZjYCLO2bU{HifMq0jVX3=TGL3>k8>(9)9@@b{)SZO-8zTM_t zEwlaSVN~A9*I)kTJ}6VOY45r8z(=KB#cg|nGiru^ZdPlH6H|VuqsXv1n(6aUVygH< z3DR4^WXn*covl`t+OCS#xbUeGuhJqH#?{50mJ<(e@(Pyz>}yR78qXuHyGSTKRNQ-W zpVyejaRI(a6C}_F zXDHtU4v5x)B2o^qYdwDbZ0l3N!YcYgN)e6$xj%Eul{3utJeukI64n~d%Gz}E&6i)oyKr# zP(%lM#j|n;27D?{gl1(9Xf_S~ph^DGFHz%aMGtYJ8Vs_Q?F~Nkcu`iE$;k*WamV73 z{;7uTn4D2_U3c!&yy#cu@JUZyP83+~i4&mDTM{6*w5BFF_bbr(B9zClhW}^REY}8M0-bTv>GFhUEQ9qAX<{_sF9uU zynTz5eQI5ljsx^=ah@(@FNn%hQUW214n=j~4B7?(9{k$_;tgw^ilLMLHdFuCiPSXh zdkr|~L}2Q`db~Y86dbykGG7-5F@e`(bQ}_7Ues*%(9Jj5h0^{gIFCz07)3Z=Vbmxm zvQt-Gwuk(1mO=d(Pj23*+`qNvWzQZOeb=C_?tG2#8pbX zpxL(X6RgRvWX1xqIGhz&U#B@gdC-|ls>zjFJze=0KKM8_doAYR2_fpX88>wc`uEb0 z?hyYK`=pLhN6u-3Bel(48jeRCf`!CnMOrc7ES8bt)cKNahqHe?k?Rcwqw$I*$hI?` z(EIb-<2n~D$d39<^={umk8h{WX0h`Z*%89QhGzk8SgAzf%^9H@X(57!_G0Fr6_}mquZBHQ zAV!lgrZ^iSN*`M433k)rG44b*=WhQg4r0_ zD><|vf^>H}AV^9|N_R_lH;PDyfM6gk(nCl|cQbUu&@~L4&3!-Tyyrdt=Nq4f4>No1 zwXXHMu0?HnW|d_EGyvJVGbyA(PkIFJmLNa~JziU2#g?L54oXHe?Vj-v)h->gf@Ys5 zm`K2~>+wQpe{n-cQ+)`~3ZZ1G9Qr41&nZh5!P{^&TxhqInEY3M-H%;&q~_Js$}X!6 zAz0LZ{)}E}1Z5X?hm2FtWgK7tg{gOgbFE`^veW##G>0L^^&8H5%8@0|GP3Q~OYv4z zQ;aH+VDAZU1|1{+tlPpReq9em2d3iR@4ix%SL5|OXA*w&zWMoZ|2^T0{(7i#0;#jt zxo5re5v6eP!Ii!-Qr{f=M46l3McrLvw6Pu~%2gJX#S$c=g!cQ4hQ@jUhWptl?RS21 zN#8wRbJLf$ye(&>xX8v~{o;9){BVsb{W|czJq4uajXCvfl9^1PJ3;H|k53mgyGHlc zp{4)hxNs->u}#8cJ>TNT6AzwqK@|A-br398XMih@eL9n}W5O!?;rKPB!nWJj;6-mDS2-X>ke}YTV>p9m0@$|D&&$!jOzE8Mn&Ezsk(fpo zBc0h$sbSb4loezqaN|Hy2?wn6rjqv%q$y(68qcAH4PDb_&YjmFDmDj^3bef}A)D+% zZ46{SuQAcyxPF4ikndawlt?Il-12W7q$7PS`0N5nPPWX@BazycGGG?-f`?j+h>Ms8 zN2{6g6^k&DcSl3aS4&n1bq?FeWEK^^nX6^dHl1K|t92&>VKIijug$Ty7sI|xP;j9z zma#euT0?(pKKH7P)^-Iz-olR~|9^tbzcuE_#Pc87h98K`VWSP=Y593QOGYetq!My0 zTcluiMSwaBPWDJ)2UEm=n1BNrWvP_1$R2Z4&cRkYhR*d^@oHkUxXDI2Y-+}K{s2U!C+ z3G{c3!&n27ZIHS0b17APsL$gT*IdxKYzAN+iE_{LZ3~@hsL`pngn1iq87;k<_n^+ck0y@RUYhVy}Ty-_1Kzp63V~IkF z=izS>e-Tb3-73j?k4%K#N>Aj_ZF6_47?}*e@98ada?MPaI}fAZ=(l>tMRmj%>pPv; zDJEex8}q@{q-)RW7F~lJC*<^}wXRQ+rOBnmf%tZ&45g^-E0ov*_$ytkaEQ#Zorznn z$wc{26rpf$`l)&o?ZU%98|U?SH*3dW?k~2{;OYM4YxJK1l=|U$WQ=25eU^cas9JWW z`BjIvclf+wgxVWAUA=F{#u1l1J)=8~%!;E0G|#+J)^y&ux*v3Squ3LMce^kr0JlYS z+)Y$pY&R)YRCw4%mX@u(S2=b3r_TX)P?|8QpR^8tZ6sfnK)fbH2}Hvqaqbp^0Ad7z z{3#_o-I|Td0vyrx^LhDIlN~)u7h*pZnspM|*|2kzNsU#U;B4sf>@{5b9u|z);orZP{2WY%|D22_OZ^JG>7js9!f)%1 za%hx5k|Mtz+2 zOifZ;c{Z```+w(bJT@pVg(cU4N9{9Cl|K>3{9(>oyi+0GY-{-1e?oilX~z@#CDcC@ zgw`cR>-sEZ`6=$4#(2PwwRTE;kK=mBsKK73wnt{CX(x`NbxXb}W_^WN8k(*$XCzuK zXBOx-&7D~GM~+6W!1ftGA_qWg%knEov zOJwUQOphOYRRaVWYwV;5nXXXO*Lk!VF1KjJ20m)(5M@A6j$d5>V_D*Rx-un0b z04@uLR3Wp#25wyj{M|2<#w5~$Af7OZnh+&~j9I#r{+2HYq^UaXaV$F?y725zm$+#; zGtZ+!&6JCPi0Y(G)q0&XUHWq()RqYkxj%s!$B+rRHSD*+a33in+fHsL?o&Y65m!4m z>TO&_*YADlh!%I>=fk%03F^#Y-sbl*YA?VdFj`Pjlj~C7v_UQ>oYASZt~qP|FH+`Y zKd+N&R2cte=L@k&ZRXak_?%jApyJGCL!8LX3}1W|@Xf^#*%tbbC|tU6VUb`fPoYI} zk!Daj3PsVdZPIHMS4kva(-C#}7XIDM1zsM1d`?Df>im-fRLNn$uU`E=_aIHrWcCmU zsLP+F3)~z|V8}_fUHoUez3uzF-meNB_emek{9?Z9J$UHQ<|tOTV};&1rCsYnqJ}fk zw1*1(YQhxjB7ig|dUxcb1b8wgkzgO*jhh;J>@6}}8WiRbG~(G)3y+bhU<;!Oq`%OR z`9k5pl7=@rSJq?6D=3hnoQ$-#pcr`-Jw3t0VC5-t7s35CyrY{2>~M3AjGOv=k-gb+ z>St0#YA1U{)4;=N2qLW2QJd%vE)5dwZ@VJ-P4!)V{A2=9h=^zUf)*KegUaEbVYVrq ziC$Qy?`K>NyuL$vBfYd$0YOM^T=u6a=ksig>VE_r*H1oSoq>~O+0h)tQIA6qbW8br zYH0j@LwduR%NZb&0X5I_Bl5DzWPw|l<=!?2 zt%nyGpDiQ7y<@dJ@!*eP8e+@$vTOrG1kG?JU+5%)V@QSI>40GCy56R+(L(Y>E~+iM zrB@|Nwu`#DZuPdh@u$ZMJH7^|YB7FNgK?L3KNDxle>=%!zk0mLP@PzWgJMT*lGfDh zdd}y;a%0;%e_dZCk$>*#d`WYW$*r`x*^F(>?&;Pg#o6YFn^ZV*{zocYf%+r22bhw6 z7yf~bg>(D8dW}}A!xf(vZ{9md=+U2rG8B>g4`vfRYJzBXT1#CfeIoey$9TRTVNLS?{$m@1XPCcZd6y*XGeFuqD!lnkwC2GR<3z7nLV{$`iIh^d&~!ZM z5>E(GG?&B4lmfNnnNov!JHOO2kX@D3--9n)mM(0aAC)DSM*$AMR`G~MH|lk9aWGY}sB^uzhU z304>T&T20AGzp7%0jEa)be;C}fr50PlYwWrtZXmdsp^L{E^hO5d5C34oW)N6Oq=Z= z)o!KsH{^Zqc0f;V;C3g0FRr`7UWnrO_UiGNsVr+g!v1Q;o&$9i%urY4;niCaIDQgn z_Vv+Lpy20u7VrFW8!UBwF6f8f$=M@5)I~rLiG*X+8=78IiJSy1_JT;&P6v9(L)wyGeqr z%NcB5*(g!eaFwnmRzA+-TigPQR;ev?z2nBJl!`z}CJ#e24th%%2X}gZ=xzO4J)F)x zf*W{3KR!zQfoKkFClX=JIB(%96DfcnwDJXiMl&&b7P!?_t3qRITuKOH9S?j?07 zZY4~q_GfZ>Yef>AJLi&y87uJjNUSrW@mQ0jFKNx=Q&3e+6wTfmo~)4KlnO`#V2&_@ ziC_=vP+r?0AP<4{OE4?zyQLk`|0GPf><`EX!jVx;<;-ul-MS8{7qOy!rFPFP4RqOr zSU4TacdD4$e|%p9PHg2gz9L9e9yAY8vM0v8nRx=sSH-gR{DV-X@rObg*I&Aeg?Wsx zh(2iNnQKmfKm@P}N9<8uUxN55FNO61{fbr7&zJ`+GU{^t=@{gusiNN?l1@XA@7$p(&wAsmuG6lz@&ew#E|9CkcV`#p z!8c!;f_bHYB?Dc?d}ttEpK(sICMq$oUNsFKtzbQ`lbpl(&^h+PAy9D0u)x+$22fg~ zX!Dwy(d{9b%%c!~DSC`o3foaA5f6*pLWutJbR=OiNrC8*-xRi;O#9>C=oB;jnT?== zr}m*TZvg2v2*p>)uu(Uic(OV&WH9jPc!azM=rr1*IH7eC; ztaUU)R+07oSp@7yqrYxR^b+51#LpJ4<~R32^#d+*tPYjeOV#VZaB=8t7+wag5OfWONzx+cJJ07ICBgj}4z7m2aXgpq~Rj zRA4;a`yP5oobp5RBeQtMx#JSE>6urr{4L%3s@p7G*03yapXb$FfE6^>a6Z-p6(^2t z?1jouEEc)e`0%f-IX{N>1ew)oPoN8{3RSo+!cNR@2z;A8vhmfJoJg6 zP`2tpgt==?OdqGnr}{5d1TL3v5UcGQb){nu&d{ief1ko7ZdIL6UB^NGswN!OKo%XQ22?{t`-8v>BZ-E(2r zzyWeF@O7-eL0==FY738J6|$`mcN->p*J8HOKol?i9LGuNc^M3711`D_?2tYTF|@=)KdQ0Xy@LUQBo?B-Z2qGMNPG9r>m=%S197@CK{99=!Sw;X}Ctm zWY2POht{L;Nx_Sm2;n0=idW84r6=)ZH#*nZJ5!`=E5(LPLe$??tS5e3W53uWY=jVTz*3N}};1N~Q8zSffwj?t=M4 zUYSDgdoO}-_f6+oPZPOe$oOLggBi?}z_ah?v|-y%>etR^R9E&GH!FZxuB zh}vgM!=aCMluq^piF)bP>myw?u`|%M=1%gM@0{o*#8xQCA51m;V|C!D?N^1F_d1Tj z0}D5!WG=aaV>eDShiq0gc8wsvO!=f`{aD2Bo0=HU9-C_($2KAQ5`ZDNp`1NBk;&2d z^$h`Rx?Nk3bBamI#>RF!CaW>~toKB+tD;TSn};Jk_qJAQh-0PVv)UYG%9t%hxa;n+ z$NEV-ThlOR7f!4alef33I>eo)m1YE)IWF(eQ8jTkLBb=;Ejx$KH2U+*3DtSGQi6GS zXKsdv6J~$OsPoiw6WVoia$)Z)L=5Y5X_kQd5nH|G8)2?o3>yJ60qUhS_{IseQld$i zjQ8fqi*;(_)#p3U817T8MpH5)Pu9_tjZZU*Q*B&T9XgaH?wa31lTRrP(jMhf`Q;`{ zvOvl2^$}mL!dmW!RDgC&_wP8qO6ccmpP#TF?s?x<80<=t z=R)+tEF*C$&tsO3KAAN3tG$rL6XFQ+1lC=^9=4 zo{%5*PU}+hKp#Bh9k4u+KQWG?X1C)hKT(bO*Lq7dvl?_lIkw4hiW0uW)}fq z0RD9joJ0>kTf$^N3GEUVqs^Z3l*;dZQJ_q(CAp39{ow+B@R69^(Nm1( z3E_9>oT)(DGFpW4STAb{&E>J7>-d$nJhNU6{(ew6rt(>XvVbUe@1GUNs3rBXG?S?! zLSp{+m~0f)7PFj4_{7M*{io2M^OXs)Vj{trNyY-ptg>1bUp=i{*R`Ut=n&u`aOJqS zXA+Dk5jWt8fVXd>XNp1*eGQM6G?m0iD~B7l9XT$?ahKNS&X+dp4ZG9u5`Xxa@wAHT z4W`N+F$cMj;at_#L)bEM%C#Xs+LXm+f7+it$ zYy@dO>?ycpfnC3MqSw<`^Py}|a#%$LEZEZ@nHycl9eBtsk--hvA}N#2xSULSSp0o* zQ-ng6=;yj?koAlOR&dV`#-pWruEdefITFweCpcae=?;6*wWw_`AXXS;rwAnHR#dj{!eKGxz#bC3jgfm%-ojIr4OA5}Ec3~Y6 zxw9&j)_ez=VN4!Py;1i99|oS{a%|Gjfy>j%EbME`4`z$HtL>XF+Nc$>`Y2D5<^@UL z4fg&GH1X*P3HQ#yd!5Z%188mDQuIoN<4aXW<~7&X4TIjGq=m=skDhzIc^s1#T02`B z;Qunc0EfHP(fM_mEFHgS*pGDllAfS8qU4Wy5{8ct12S+is6SNVH zoN@~o?0RmlW>$BF4t@*Ps0NYJj|BiRD@hbzoi$v~b_p@-MHr??_yWuv_vm#5B%h{} z9`my&X@UhyrFwUO8q_aAMBUk%Dsw}$z0JHXs}|*UUN+5(ej7H?fEf|s*6Og8c7D}1 zmW_<}oU%+}m_@Z}d9!pFz<{zYreSZaSV1`3Jqq0H>2$vgCSezi+3AJTC+&y3Va69>QI@`XZl7>F@{+I)g<+Z4}_XPg){{O?^!cS9R! z;jLji5rzbONm4h`KG?|*G&UPq)*-Z|qga&uyoQA9w7dU``Tt}AObwu9tw@0tFTbvw zFN)K1!0kvm!kO1E*;p$)i+;vr4~`Oj`X+np7o7jmG#1Kc8pk~i)GdzyJpdRk%GLRjb&Gu}wvYYbh1K zNJXuFX(q2jyR8*GN);?byhKEFxp;g$@kcQP*gdWVMK<%l;E5NAo%68HV+JP)g;=6X zL4NmYzbdC2s(TwICpjLf%&!Q<^9X+pe=9fVCfNE=R0U-9vx}BW|9C**+m|T^QnRas z!G9P;zeP``r9E0dj5bVKexKNr(Xh7h?bs%qv63vw<+-!a3QZu~M?NND(>+I+5Jkd0 z#dqZu*TwhE0;QZdV5n>i%wqC>aP81KkWTA4NU#RTCk{{l`9Vt1KcgALaLL9_na`aN zD78Nac>Uxc;`|mB*SbdF=l4e!G7}S_=p+pP_R%HqdwwDCO_KYD zkR@B-k8M^RKjz}hy}Tgh^H=_+gbG5sdcnHHLqrXdkkuR0xcjJEty>3|^rnHjz#SK4 z7YM+R@8XZ2>v3UF4HZ|EFMg^tGI??4J6I3Bka#8kL_Lqbz*Hx@v4Yk<8vq2lkeGZM zFl|FWZ7yBAS1Z`AYu?ox2XnC~DSI;CydJfP9oz%O{9rSRkX`l3^#A;2F(vXhJJNb3Pc`xatPBf=~SYXr26(Jpj^u6*HX-WxE?bzdT3Z@P;Unj{NY!F$qY{99DC<5^pRIq_WM7)hhg z;!c0Q)GgrZJv^W3e3(nVT)q3J@e&9AO1HW}9stVY20$$M+io!2Ahrw0-9EFCX%6@`6j@+5gkHJ1U_v8NL6b3Gn-$c}6y#vD?#0qn9L z&I%qwJyJ3C7eSxSTC@{$hu-LT1{J6Dg{8*VI8a}DKcL$p!O{oybF5VdjgMs9t4t66 zVo(@0t;Gn$(3dUlwSdQmS!^CESn$ZpbtH$mri z*c+qLwftDXEB9fvp6@5m0owELBE0|fNMc6}l#NU09*rQn%;F--6X_|1Z+1j2L< zi4{(FPcapuYCrD|khnMC+|<6jY$~&ng03aOAOV(D*Kll9Zf{r6wG!ddyaq?+4cL2n|h23a=|r*kV~dig`K{ zPEnK0se}pLF&vqG8GV+`Od(4L3!d2VEO|c^IGzv+-{rUeuvHHV@M)-mV=Mcb;e!TD zfq@I%?rzS~?$fnv>*& zDrD;K_bf7!4TWZ#WkD?6K=u9^LEHu?0J6BjqLHtQQb1;|`N0nfQk_M}{Qks?@s~meh3(o3<=L4Zu z-1J}D{w|>$$kS}U-UVMW?}*cfxOSEV+mhy#n}#_ADu|17m#Ae0K;FJcjLqs_?)^eY zuwN2xZcmxQ$9GUw60qsB=y^ph@7G>Lb$y#aL7hYKfpzrr76MUElyGjR8Ip{nW2mV< z9vP2bfcxnYH914&hyF|(HYu{!Sij#X$a=HG>+0|vyu-^(k0?2x7RaK7Wz)nS+mmXJ zY7<#hd9GKYqQ!P>&45rwHkS0`7*nfc=U1oj4Xovr?6$?nUvOvSsxR3PwdpM$w{ecH zl?~=fN99_*vn55P;);*2{cYSzTh{^a-BLRG`{!RvaYc9wB%?Du-r_2}U~+)kEqNYn~>=(lT-2IEAjE9rQ)5_F!3GSA!oo@{5xqb#` zuH@wVE8-OzaY45vr0YLGL}Q8gInKWe+OF-C;FXcj>E8EyZ_>|49hi5_`OpH zj*aoJcJk*;PtUvl0KGifxDE{&2u5jjP9~l~ga=BK!}%Y~ape+FG0=>C z6mN37~E#dl0nzo{{)t zeaGvQ6buoVJWdz(3yZ&d<^nmgL#&MEsB;oMdo>yEKI6l3DR<3Hry1l6OZW3kBS~>>70=E8W2HN|MkdyyNW;dA*CTwJ6T{N z;<5^wS5%jqMTZjaQCJ?`V*1L<&;Y#=wHW4K6im5Q<>yNM-r8Bzdex|?#b0L@P9m{p zEgnSXfho;?+RALmT6l@c5IU|;nV7OTx|TGkAWh^1e9vGhSHF?!c5){}YnXmuyqeIp^ zG!;_BK&d1=@>sP`>HZ*Xj^5@oZdOi^bz+)_%XdT;pDuk%qU8m!|CBHYb+0f=yX#A= zPX)w{iY*q9UzMf9@ID`WY2By(^`{z$XUGV0xfk?n!J)nTqR=84?h zi=J#DfE6xaoiHQ?qkGS|ny6m3FKFddPh9^9M~$OwvnnXzB6I4RQ0D*g`9BL;2Sl4~ zUzLZz0N=$?uka0&U-^|nojmiY&xVGxU? zgQGYNsH4GR=c%F2!t%?kyCY}+k`+mXs-jP22<@7S3&kON9igoN!nmi+cRjiP{j6|kGpI>&8E~_C_W#VPyce9 zE~%^a6wIP{e{d`h%1vf?*Gu?za#qf1i*t`{Vd=;hiYcV>WX{J6 zXH>#by;m3rk z^hUZig&nHrE(b{LjzFHrf~dms>hpe&nKvco&_&+m?{ii`+7Jiq>#QVh=Jjs1{Ipzg zpPz%naFpx{eLYw8+yV6_o5)CXu?y4g{9;}`Q8Y4>=9vX0{cs6uz9=ieh80;Fdnik` zxVRVnO$KeB!uBl>l$SP_N0vZ(DS!EzjBm*%3G1`h2cP%@_mB}*$6egmVlmMd#k>ZUb%w}v2u@h_3oZ3M9Vt7_d0VsB-+&bZCG zV$R`GeZ#a(UBb=9|2CGb4smHJdEhVlUudkW@QKhOKnaob zr4Qu26J=+UxQQVo;`_4Te%;^`OG76E+`sgYJ{JDhVwcqxRM4BR1CapyL6r!GR}(uU zV>>4k|3`9~a_ijybuFXOo|5s!J1?Z~#kQq%dZc^H96Wu1Ki(mR%TQ<;oxX~BL>M`T zN&Un{yh7UsF^f>7l+GRmy?HH~VcfF$;?! z)5fXOK>GH2qd%BIfZLKzDdajm)`wZVFqFwbEDUUIB8^7+c8kz~$)f@Em%nwEC5Na6 zra@fH&RK-lI2vvC_tCz_{F8qy)uJ0(d#jmuTwrrg3}<7f)>`f<=ccVE9*`HhVk{If zt#WZi^3x3);==Pd!m%RaY`j8SZ+?~?35-hG79xhO1u=2KWEdC59;>{X=OPzKWyEs+ zaQ|#fPS9K{=CU#%_(=jUgof_))#y$OqF`J9llRF^^u*V~!8dTnisqBQ7yC8&3#}(K zP?Nx+yXz}9sRqfjg<;2JsZ$%nOgWxH(YkN#t1qNK+dyC>nWs1X1ewE#`U>xbvnT>- zNHjTLpIvNjX>WC772}Gz$2mtFvmO8u!23~LQX8_YWPX3fnAO=G*(`!>3!z&FLZat< zM=!(81F_1j;v(APZ|83^i$w`6D#>aB%h>ZDnKa5}6fn5*NzlTMPsokGpE)Y6ezssf zj74z#9M<>Vl8#|-?5q63?kk6g);Q{(So?R$ek+%I@CLWA9mXQ^Uxtx=I}k zU%Tp->It&ySh}xFCHZ_#b@}_47Dqg6ym@52Ylryzd8%v8=UF;_jZqJV4uH%e7NJ^d zTq;<-)5~SY2FJQLC%ayy0zDOu!dzloRg2) zRKaRj&IiJLTdiw<@MBq6_@`a;)o(=rPh+eh4ic*~mJ8c^lrSB;cGlOib ziy!%zT4<|G23W7NMygND4H@QN0lxEyfkH|#f*6r4087u`@EdU}W1e#zaU<31|HAhR zMwb7J`dVP3_cvS|M`S4842An7%3(SeRo}LkdKk^IRk+w@2MsklIjx^giFRyE?h04t z`FW&wkd6QOLHe^NH`gl=X;x{O)3b}@yHaka{QG>UParQH0rbC3e%{~E{Y~S7@2-a( zho$e)=i|A9bnXyyh@JT36W3Kb%Wm|EpNn-7J|mj!sAu6Amu;RZ3N(YCw6D=*$j33> z%C#eHKV&j*M>6vZ?+&0rxGngagMhOTK=Okp_)FLg(WsXsf&!Ho`$VE;M`gFwUfruQ zB!&B={QVfVuHDXl2M!(K1_>xGxcR~`UF9=EOGi?6rz4y!eDBa@4%#th&=n_-a{@TOVJt%ZaW{g zu2pfFQYVATqc#T>kui8;zHj_QGA?PbNP@PLpObupNzU$*>(cSNXl&BX+&*~tT!)I~ z!ywVtW4>2ijc_N>IP4?$Q)3iwldE`1&?n)_VRC96EKh%xUF~9xYy8`Qdi7xT)^W*; zn1Gr4Z69f*>FrtYy-X|R{n6<|u+7}l>~VU-8nv&5S!_P;PCV^EUb}^E0Bs4dZhvC` zvPM!tXW>&idctY!t-o23 zgY3e$r-kE~jq%6aM$g{#0wseu`+@k)E50s9lDSdkMaxL^$L|_>vcBM-1@Sj;74aXF zJx=7Pt~br%{bUJ}AR_5a{iXnw-r}_PmR`lRP#*QL$TAm+I_v4H-~h zJ;+VvvyZxV--7|+{?PWvgdAzTbrw~L;@$W{-E1b@Z$oD|3 z%x{Ryd*B}?l@?J~RfR4fHO$41um{8oup8=O*7?jwYOu+C;qs+vL;Ta}jja+aLpI4_ zTU+XLSo=39u$sg?3+?pf-z<^T!GS3DqHsJL6IqrTwd@l0Z$(|++9u{pRr;aS5ZA`t zr5Abo4Hy)Y^i#^W^HKrXiVOPGw4D3)0NRS2lfH%!Edy4-`Jgk{2k!2scMP1C1dWJj zyzs4*M(HoB-=A;v71SE+VFU>4V(`4GjfP6i-~l1Q}3~z z{~m1q*^Bs&IpXyZ<@_$G%|K?|gCS0ZzD%d&uw9mX{QQq-*R>ke^h*l(g**8DH&20q zqxh_?J%wZgQM|R!SX2f1DUIz3kle4?0~(l?3T#8JUbRkZyi3^Q^9O^Q%d=; z{#`jRU>mnC|57SsIyo4Z2KI8q==tf(DS<@b;(~XwT#cn&P6CF)Q*`NiWGwXd z+d-9oa^s^OW*}3EuJdaSI-s;K>(1frWyB<@9De$TUAx*9^3 z--Qf%d;5zWBH1D8qx}Sy)W(aM+=k=Wx(~l1O^@9GrL(Nkk%&__ZPeBrH>t5^)!?r5 z{;Jnr-J98Cco?tiJ;N^F3gR@KB;&W=`WE9xl{^ zzOye^?K+pYd+;9-;CHr@?)|gEc^->dn)iR+_|hw}$%(CObF@gyT2z&1{7CRLN!@VZ zL0cx;h27rm&G);sem#H8d5@+xdtdRFdl^s_uY=13=M8=yIigOs*E5p?1N^VX zgRKy4Yvm8m^7&Wt-*J8oC`)CG+-(#$sR)(T_PqUMA86wa4O;wiHZd z9i;lY{f|&3`mMt<#JN}r(~#pwqBKpJO74zdTK%g2jWiz$nesRT;)Z`IU5AO7l%5pD zG*-_H*&sLYcB&Pd=CHUDqejN-H#5$Bx%7rtb2>vHq( zU?6U+?7M(@c&+nW`kK0=A}#$WEasBzB8P& za}iK5&9@I{(OsELJE{V_P-Z*MmQ&`#WM|*u=z@D~%nQwH_OPznJzbPOfLyh$=*Ri` zJ!Jm^_=iA%btSsuN%u%l9RhdCVgGkh{udd)^6UwV7|(dsav&D#q5X7z3=Nn?Hn71o zw*Uk}A&7m6^?0GhyN(S*Z})C0o5LMe;z!!(^V1Z<-e7?QlTIoP858_ zJYRZu1+;j``HkQ}!KV7QIX6^UH0jM|$42fbDnenEp^^`lsDeBl17v2vk%X1pzu`#% z!IX$kCiEQFpJSdAP)vX1j(IEPE!xql`DRHZ3VDuUCzpr!dem3ouQ-ar=ZCRL$!uU2 zJoUvAG$IqXmgEaUWm5WEOqP94rk@$Trsxcb4)1PhwR#wX_rtx5GhYkG0Ap~-aDO{C zvo4i^*ePF_1mn*&$4zYGkQ*^pard`$4K720OMN=)G0OV2N0QPia)8#blz?SePM+3k> zRA~RyeJ73LLW6ZF%q<8OgBHr`5gq$$s2};5HZUnfVO(d}*cN;rK9?eB4gYG5 zqk;D>!p%@_X(ui)%|r7L+Eu`@g?Dk9(J_&Y4*+1&bf+1&KB}d7KuI*bhyJ~R-cCcC zTaF?r_U=!2`?*`1123B@dX~krhys;XrB2TPk|I~ic=La7`O+C+k<%|%@PMtfgGh>? zUqZV}Eo{GqVLlcdjUYG4von4m+&mICxi_mGWRDn?zTCeUkiIl06P6O`^GLAN%g@F? z*^bCdSdV=wafMeUm9cV-cVdg(;U=J!%!qDJt7#jGEm1r;Erm@q*7fuc8RxcDy~UDj z2=<4Q?LV`A?g1wvg0#NLL^<~61~LcmpqO^?xsU((mZne5B1nEDa2g{d3pT6rVdM#A^P%U)UxQEJ+`$?rDJjRaY-Nd$&HgHvx^gZ>}^IgQFE$a?Jq^620i|Lq25^tD< z!1~%tl2+;h1l+A%3*|aMqd+PpU+q0vjpm%v=_D3+czE7f@ta-4Hj^Q?+CohtCl05W z%i}&Rv1=>ylF@sCw?vzO8C@)>>0FV|iZpeQ=)RKggvDh~DAAu7NLAXV9PoSD$~S!( zymTJb5N~ag-x>J%1Vl9YRz5hm^S;=@;8M8t^5){_Zjv|Xq*zk)wVk7iqz`>xK^U|ch|T$qcTK4Mm>A-Ko| z5eV7Z-#gb&w}z}Pr}|bz??ve;)}S87zRtk+^k)&#c?4>hQ151S{w0Ru$ufDbYJ^*S z*D*UWa>igSyCPCtuGemd+3Czdt`r&DvKsyigjwGInSeRfq>?p`G@olIYvMU?Q7>Ec zT9b@~;%|G5CkoB@%J3r0{H4;$nvWGn%VEJ|!Gs_Y07zawk}5@;%oiD~1o%0_A+%(n z*X;F_P=|>EbXn$O#_7xSPzRSB!?qrJWZo@l07A>(JoxyoXx{ty%N5;W16vPw7xcE9 zd0qt;=z|#Bx|0J^(~a-)_2`3dj}NY;Px=m$k-pa*>J3J|3+k4z5XgNP@s-dP0&Ph; z>Rrc%f|)`;%XR|3jOKQ?qFL_<-8fgLcy@#j8tobA>K(R`QB4qO^cJDtxPT=yYPVlw z$lUbWXSDk~hfT8^@x$nPZn>mLhUnDedZ=0GLXrK24(j147O^}Xo|(oE+eefrg(F@H z%b5ei@ycdGIBu9vff>juBa=qQI&*8QGy7b=nM?Ab7eM1LU{8B7$LmZb$BbyRMu2=9_a0Xk82PV!n61GT>|!@>^X3u@4l^PH&Ylyk-rLU?4 z|D{+nZMxsRxY^sf&)2KDYh?p`Xxk9!Mr1GacIN7vrmpgs> z>^hzbIq4ROH=K>KS?BwEUvW@GNqevU$EU;Up8P$2`21Pq)Kt|Huf1vHZy?$>(|4N9Jo$BY zJ;4L7hMS(+c1>97dymCR51x~1aF~vYP?SEg(>>Es{f|#g`ZdFi{k3PRhb}HO6zPSNP&`qrnrXZ22KGc(R$&?!VjV!5k3?c2rxuIuw8_@B5H^oH*`|J1) z=$x+tO3qC`ajMfwem?+`Vm_*a{spk~V8n*l{ey!bP{RlDQ~0+_Fk*iHozLwINV-07 z|2o)iKIlqX*p~)v2u3*Vs=AC5b$|8)qjm1)#vN4?8S@t5kI z{$=(aR+BQ(-?ZBn;`|dDB>d%bYzDP@mxWgPJwF^eIDD`%bEwXlxAbD*lPmIy)K+&$8-J35lgSo#uIz4#L!2ejM0ID#bb z3VZU{eAwxw0|GOaE%n#pTkD@!;4lFGs+_bvy2YpZ#GcZaVWqP@q8_G}>{lb+3bvPm zIo!4&I^HP7arRnwtAC>dpF?7vz9i(DqoY7cGWC*0wd4{F0%9|8;M?NBQGv@&-{|5o zy867|LC!BgU&#cWd)RQIYQvb^NOcHIi*F&LOg=zl(7DY&$)!z#Qqgw4;lzwgZ(na; z`uoE=-yLAHDS_W!Q1d=bfp8Hx5!%f?AT>rBENY6AZcWGDS7XR>WV;%Z0s2MjWnf+_ z5kF2`@Np3k0<>SokVAiQWDWIC$ege2weJmgYj2kZI zLMmqjs3a|Vt*o8Vb$~%vTE18NWF?w@Dd;FV*cS1>*n97&Cf99!6hT2nMX4eJDk>^f zs?=abrCBM`1p(>3Cjk+Wt|DOQ9g$u`O;CF8gc69-krG2mFTany_u6ZpvzF)F-@X4{ z$1vk#jC^_PEblYteBLAZBO5-KKAh$fIZ!;2`($q#HYVY znQ{g)Gq(*pP8>SXr2XvsL7h5gTaP)g#(_7ii@Qan*{f%OimHozL0O;I4pkkP2n<#_ zD`DC&&{OeC>FbrnqBB7}+uBpxCu%NV((M$F=;B*r(*>9?k90+)_flG-nsLN4`vKXX`FSOoKaS*KJu4qa!`CSZRQzZP)0*)tpe-0+tShn$&Fd z4La~9XRrJ%zih$PdxAMiTFsp6?zyo#SB~$Q8b>q{?=D|=J$?WzZNp{9aE;_>PiTB8 zZNPExmXH+RArYn}*k1h`=MkUxr>nO#_6>WQ2Apet_d4L>Aq8QZ7|xQ-Y)NS+_YO;; zf@iu3@D|=aJOriwc32=y;=HnG(#TlJY5PF-GLrptoc6IV%HZ*3t0M}?gcGN1Z7(fL zmWr;~-D@|pOwwqzrJkrByavR=&=Y7w!w|%E$vMwB|22&xu3_l>hpiLl8DG~-c{R{7 zyXPcZyT0>W^TsCny$o)3&?!ye3yA}$hT%v7_sE}XY zQn-19us(o?=!Tg`bkHS`C(VVp5%jspEipxx^0fjtk_IDnfZ8!FLZJI@i{VxdkCg$j zl76Cx>&QwS%Y({e=R$|Br6ixSOXUu#`#$uMO8!)=r@}Nbv=X2qyp4MN z{rsJ`nE8m$MbA<56WSz9nm5|zSj-lF?or@OLQJ=|ittFb*uKP5r|#2Qn8g24|C!s1 zDWnLDzpuR6{#5rU>)5PR#vYwVDq*mG`N-44)h>%B%<%{4pUO2#1VM_ru`txE$h^T^ zf@|=W@m1C0RpKfB?@lrXmB%=b7oP6d;h$^=0?wmQwtQ}XhRdlqZiVxKhtFf<+xJ5)VE}U5pn9hPkCp@a%s+oT#WRUJJ%hY3?10=Y=0L6Hu-7yrC0Q;N0)71qs zK}lrto?omoWdy(4v@BD*ED;x+oGTCxo=bQ|$bg%Ahbuu4& zTM0H8C_i+`(d4^&T`=X#W1pqcV_A2{Wd{^rn24D+ZZ9goy4NOwA$$)c25JWnTckbC zulUj#7`XEGn#nZBvG3dR+kS>G-e0dUokFBG=t(pV{PdG1)zYh9v5H%cskV7D3Gp)r zOVloj9leA#c8;=m&dMvW*-SelJ=$1X+Jl0l!S!kUXm0L{{Vk4zbfx>)Wt}yiIimw% zJ&Lb;2CyJmObthOY=^2mAS*dh*UE(|ZO05Z_ALLU&=iacZUExZ)MwO|tzWa_JL9DF3)0%}10ko}ICY>qF`^%os z=JXkCe#p9`vdt0G?4dnnhnF-q ztUvr4!CB1WJagXwJ6D=8#A?^8{pv%`3%K0=?6a!%5$vsZUU45BysiG$K{VyH#;sc| z09DixsXI@_!~)I3D4yIiGk$}CD#P5DsCgVj1=%{E(Rv*(d>?$ZQ2kS zpsj9X3KL_ip3S!(;J=w%RyS_MT^K32Kg|D)f0TWQuc_Qk&ySntuSN(LB5s~*Mg`pv z+pb4gHhi_T+{@)#HF~T*zZHR`YEB+ae}2tXFa~spd-VE`IHru%KGM+r2Pe9wuvdlG zHa7th?SHX*IFDOA`C8%6(-vIV&WDynANXF-m3%akNbw5!9LF72FX_||2Du{u?n8#F z@~~X|nFsad?{(hq9zJ~1y>6QD50-mXN&L{>T_Om~(@d5_QR*Mo+06!E6D{PkS36_i z1V!QyM8cB8L_4hLk)mV>Q&9=M=nUGi6KDJBOZq6+Hys`eEWj9BxC?7Cw{t9J6sn#u zec|>|EN6Oj9t*H}JO*Q9s;SvHO$Fs)qD)4jadqszcRDSlqVw$MC;ZO$wV(74P=8%H z9NF}p!^Fhrux341JpeL(e9A`-ZcA?u&0PLLwxcML?YVA&!kS3?#>_-_ycI6jeF*@p zhGDe8u;d3xeFKpGac6~vEhT5bL3BB!)CS$0RoP9Pi^RZZeSImZ*c#!W>oW|(GM$R= zcR)4)_v}Edc8=0#-atD+?v&*<{i0Kb~ z@nL##dk_Rw>xKti560?rky~Lw-#Z=i6Uv+|kIezkZr&kFwhO zsnbuyUgo}ws_4u8g;k#QB{nqwbO47`IJ>@PzwNcANy-0adzRNfChc3o8b6l>!cIrT zaB(mDPsAbc76_i%?Pz57I(_^X8O9Rp;lO$6P#5&BN_abPY4|^Q)gE?|RPn03|CLzH zD}dfi3N0d@o+bOAu~2>Ia6Y69-CM}@Cjiw*ya57Cvu`vCc5wgiNGCcm9|wjof%DO9;|Rh!K#phmVI`%@60s`lUvi*ETwa*_9E&FhEj2@gDYW{8`$)t*`+&EkM_2%;o943+D=QMyd zb`^miFtVP#?-d0R!>*#K0NS`n+BGMoZbU)}V$+HU+%DS|NNg7u3k?idh=V?)QwaVo z?@CP(UlzCIdPvRUvCR^Z1YjFGVjCqv( zy|h57@Gc?+FUa$L{Te;YRGm=I;&2ToMqId|U_W-9S@Oyrz5dHRH1nVI`tpOymxJ0~ zOqse_yyjXmJ;kOhm3OrXQ~$oq`TJ{gV_q7SX-V7t)br&6OWT{@T9$rxMC>p9vX`59 z(OK4b4<|Hdof4`D%fvSrwbOK9twGwiuBgKcp-2cKI~(ALOw`t@%B< zvf{@Sk2vK(WUc{x`&!dMl&7&4hhN?Nh$bvj29iD#QM=_%2MP;x*kzxkekS#MiRB7@ zWQy#mvJHM5cM&b;e5JeOVaT!8H^+{4VHvWN?_5H)(fhKZN51}$TfO$cFq3B%k#gdo zj$4Kn?RNf49unvVVeTx&Bt#Ehjr2e;dTE7G;ZbkA~qT0rA|m3f!?wKN8qA2q7@N6dCBd1YP&ml z81x6%v@lr_lnK5y9!6G9(LgKafy;@JN9uQ@F;5@y0DM@uVVk#fgN}5uI6{^_Rwy@-aR(ADkG5uZMJzjc+kI= z;zl-mm}%-&tBd^Wc6*_n+KRfX+h-` zxIJf%`1X@)_4rL;<#PbgAJqf7xzs}Py@f9^%bM809{d<3Zgq`gEM%+l*GtfemGh^a zZ$1ynaskhhrxKr@da%SXcbLQ)dMnd=g0*?jnv~L52ZaXc&=X+@5%VZpWd=}HhB*af zPY1amas7BCEgx9}o-ah!%|Lg@R1$r#U83;Vzy*;#*Rn2-@9SRz+gQ{wRVl_m)?PA? zJdMws`V&7FX)c>vMpMD~y$JXbYA%nLxs2px^iR9EV94V=U%mO-0svC`4?Aq2oy?86 zbm}M`mQL)%<3Qve57^RrWcZsEFA=X7sOi?R(Zr?GkjGjhshf5E9(xta;CFYPze`hv z87pspJ!g1^3wvwvU|+UfKu|j$`>$YT>l@zB+J*{cxh?La9$yjn2hR!d*iKm%9|7C|rp@KYgt}4Um5ACB1!SUB?cA<+jOwY#_qdc2zN1 zC3?$`?{-a?^B^w6yLHXCkG#q3J`ninRrV!2waUN_ulOMkvZtyp+-+NJ?>d675~apJabI3YUhxwjFs(;#5o2Ozsb@c@zei1G57w%%Mnc( z69KVz;y#^pD7u7y)x`2D!my0MdE0TK+`=*AV#3L7#Wp+bb;IXh4_~<*G2BDP!U&t| zsW6l`ChY}w(}kW6!ZyKFY&Qc31YY^{H;Az8!gAw{AmL^Tx?(DnK5ki$ucNJuG%#+Q zdwZOVtCxU*+Aj4MAim!@hRM?rs9dk8`JUHtB5%W;ZP9;$pfaL~HMT>R)j@@8@6coV zM*`xdYSpr{pi(5hAOhjW=G)Da&d`SOH<2UD3Vi;3K4SJRa7vT5oK*c;)RkFB%;;k6 z&^|3el-D`+AmjIe(TY9E_Y-`>Oy-~Yz&#Mx(UK4=0bjqKNS;Q>;i|}Op3q9Stj0t) zAJZ$iXQ)6GAHHEwgPo~Or~@o8I(br2_rm14-Fx1@5`3WzlI2L`R|o8DJL$vxohbS- z$q$4iCU?}mE%_mqLy7C}RXlfohIwe-le-K3)nE%J33E{>0>f7u2k5w_u{O+|<;M+5 z9-n$6rfNcG<3SL0i_?k_s}>$tz5Sl&4wz+o*Y)HrKnI9n+q%u>8(Eskw4-K}ooVV1 zE|M1fGS2`=+cOY)`>RG>yt-Z%rVStN(}{HnSZ_o-?9dXtaRl`3e#Auv#DmRi18qzB z875X;e^R35^VBVG^VQjw@SzL_ zka&%uJB)1974`I$Mw3s>rKFZ+x}YUotMP-@m+MPJdc=odcLCabqNCY)XDX7zdHI%x z=%eq|K>>Jj+4b!CDOxT#_`*nWZv?YS*O$QBioQs>Uj~}C%cYmxJMFv>rk-Sy^EE+u zGRTXyY>UTJ)?z!VUg5M>dr{4q~loW zmdnnf!s_$!DW|%?fy%SnFZ;mh+b;`OFa?tj4xmOMm@Skai*VVn+!7+uk2SG*Wi0j` z6=P07vTas>Ka^k$JCg3H>y_{N`AN{6DCfrJwheu`@YlZkB@$jcz58bPI-2wcgDY?& zO@;Hv=maSW;ptrDJM~9f9`ss7sfrFE37t8t*TuC(0gA6V*{ZJ6mg0rZRVS;gy=tJ~ zOk0>y;cIs*kj_q<)fUv9?sFZHx+-JXoszDB+swJm&vhFrRf@IgXg^Vo2V0LbvqgP6 z;crzeKn*MnsD)!g`8V9wMw?{z*#iu%Vvtns6$|^Pf_hTbJ_WILbP@*sIX#?Zibfl` z#3PC9bJ;YRaX8Qr&{6r`L}2c!}Ay4!Cu;iGrD-&mJ_A~>#L2s#E0tRU=4Qq zHhDxPP`@HWXg8F!)f9g8nX3rMVq>B}qzy-M{Pl{7`QVwLy^q0){b)yAXvAE=uaJR1 zxBzZHU(Id!v^zfC%q(cJM&@C;gGPMl zrUxziH7l-HAaxZ9P2wj(O)~ub2RfUkYUoQlC7~cnKy6Hefxv*XHw)jUvRkQJVR8|BU^WSG(o57a#c2;dV`Xe2-1SJk*T2NLK2@!(~i0qF@=g) zEa6BTAYy~HZ-{|{un&jK=*Lb(nw}6wHe&`9K95ItMv#+rAIw!Y%$gt{o9iWd6P``( z<0=j;X=NpRMG65c?Bz6ftAWl7iB2}yYR7(30Z~qQb!P@?)>5h6{ldT^)L6F%S(-#?jh}4;-CO*W~dHQY$O!b9(2v1vs zdezI3aZl{*Ox4dVtr35ch<5U^!dBZ8yR#r2_~T4eV8c2_B=pp@S3xbk<5_iJ!@0B6 zv4T=f95w`cMoPoR>-3=fmT|4nID&itQjH0j+Q1#sLi@T+&y1rnTthYtS2qf?%hpDB zePVdxq`269i2}h#!j%CDFOR##V61PRA79vh959>(f&an(|d?RyX zOFhl~hY4k>+Uzl*rjtZ0%%;(JoG~{_N#51VrW7YFIVr9IjL8O$tHVn;>)2GMdxqUT zp&=hBItPoWnA=hgCC|Klbjp)dU;$4pABtHLm zZ|}>~joKl;9@L9`@Rjre?}FILnVjq47iK}u%j31F=w+%y7q-Epy0pMs=`!C)wU)d` zvR2a)1@`ml#@Ld}r4yn+qSfUgLxsiZr#ot;YE9Y?7_JXxjourYL{hsyT5VzZe9mCy z$J94{%`Wk&5wIgf_xq3Bs z<}x5X@ExDB2Z(?!j`oSgVjd6ePUiNp<=1Z73oZ=n4cv%iJ zIo~+LE7jmd30D9U0f+~D^GKWZ)A!GKLg-0#+A2k^_$s_ofm}@QT5B9uzc7`nyh5oT z@hT0c&2a$oBI_AYZ$Kabu7G;+HJe43`+{mf(#AU)w^#N@TYT+{d0U^Z;>(p*8#X@R zT8(OBd&aqfU6103ruKY?mfF(j0ErsC)!hC_O+tEH4Ju@X1)Irf!zp zhQR-+eZo8IIGkPlicxcHcLgPVk~YbA<+THcuCU!piJ9(e8lT_)0aLnz=};(xYWSQc z{ESJYJEHiMjZuxdrQ!2{1F%dd^wAmyo5n9YBi!=Ry>C+q(JE4eXTO+W2W#S|&u$o< z(I}WHba58RIreT#dqHumzWK*m;}!Lk8QDkoyhQz|&*0j~?Cs7H({C}k1JzcmoHc_V zgftW1q$96(QL(v~ml~u*VXGtgJX6JAyiDso2OJSEEM{FARy=gN$DV6FnQ(yhwd_=Nt z^wAJhx>K2+eVyCzpTF>rpXof*2|e9;juAXqVj8eL5(~OhIW7LI>Jm!h$3SkvvnkJN zyzHz-7iPh#TES-HVK+N-qoRs z9_udT*UvbMVKyI!8k5D--MteRgj z-M}Th{b>rk%96p!{KtqjGK>xpcO{FBt7F=SSWMS!IA-dJsze5AQiP9g zWWVx*M;rWWj`l|QtRjfJS?eL$L?sC1u_Rt;@s)b#$2;Nj`&S`hF@QI9K|5K&>C|HF z>HYt1hbcHu@_`j2S{*}g2<%T$vZH)WnkoeMbXHAZIn{`{> z=IbjGp1~)cpj+)cGgak)>IWRRcA<2)owg7Ak45~sJpc9S!IO-b($y7zBI(=duz#?9YW1=kZ>a}6?8_0nFpQwsl$SYp}$ zi%C^J#R^xg@<-?Y`>38=ICEf2W>(Fr{>V?g;w$?8wlG12W~`9SIAEs4P4j=L zgSFLv`}Uc^LFPwi09?jlyXE8MAub~UW%_f8DC}@<;p%8zT8R@!4pPiLif}GQt{*fPWvXO_t@e_Yu~m40!*seY`)RHZUYBFgp^%pX_I z^}7}|n3!c8s3>#r%Z?F;Q#tU+Txm1b?H6y7-=vTc*X0iW2>|@}-K)W}CpWWq-KcP` zc9ht&<^EKiu$a_O7I|j=>#zT-2Kn>dIw~ z^1D7VG4FD&3|ZCL^*xoSUEI3sqtex#>S6K@ zHX)vg--xW1=MVmVB=-QLA14&}+f~$P=37Dg-IEO%7e71?=AQi@#C~+oeX-}y1pRk? z^6zQUxd+(zypKo8|HCSt06G-eTz2Yr17zAKreUdS&hk5X+_#_kc!sok(D|X;zxzsX z+eHma>htX7=HK@8;1fx}nOfAZy7fCbw)5L_I2uzt+JE~a`wl-iDQq#kBJ;cH*T~-U zne0YS|K8^bCbK^{X%1Dv|Gp!1pd*Tm(~1SM>w}rulU4+^zBPtlU*L8hO6QA(BG((rt~TaVVdy!8L`y>6Qi;58wiaUwq?Sa9G;=OZRlnD-Vu>d$}R~ z|6asDhkG3uU%KA(n8A#lvd`vea6L65vKNkt;gHGHsT(OahAYH^$f6^F+4-EjZ0efe zN0@;pxhMAN);#b-(#YKF^T6e!y`t&#fks^W+QYNkfMPpb!|s&})k*Q8!)(YqtC6%P zRP|gHS-;_&OB|)tHLOkd$F9{r9r|{kVzjhcKnefucHMINRrsv;<5Y5B|MyIr_^0~O z_`0p`$UfpB%;-z#slVH@a^T=LqmxJs0^F;svhm0^hqS$%EX$hqXqk4(Bg}lgEL&+M zqb2Jmf-tMH6mz0QOqtpxx+^Q-Thkkk(8&y;b&U}-*d3KC&O2YH>_}M?z&cC1JT!8()O%mK**v`{Qt{KW_ zmr@SnPhP_{F~&+ER{CZykP`*V;kzF{lO=rW7{f~1A zZCgYoM=MTM3Eh>q0rEvkIv`!9avk?$Rp$&xmV<6)u|P;;%S%-wv3f#{o%zNtbsa_Z z-S$J{iDv#^(bv|e&Pf=VYFp5D73Gp$YF< zx*=!IeYNI14?9wjlsl@HLpIAE+X(~z$Jzehj?p)6Kog`ZBEKhmBa*=|EMo0!rAh2! zb}F`xa7@;}*9xZLgZ4%mz08g8dv##DZ|x?J-&KMZlvY`X%X2uj z&VA*oQ&~tZGYNIeE)?f}1w`A)R>CbH0B^4iGy;yO`QEXQHV~8cz(PBk-)*^g2?mOh zoG6*N52&-QGNHe!Gdp$Wv%Kx7QE{07tT6$$G-5PzTCCV*NFo%sDn3T@*r@${)v5zw z^(IB}5?4-*4y|2RcAwEheG514cCGw~nVVm*^3U+MUjBP4{fC4Ot^>MwAb(VfIZqk^ z!EE1m(e}iS&rqdz!^na;?+IJ%^DlU@5Ek8tfU@>eO}#eB&X)KJ*>0be!K-G_uY;9+ z)7VCSXvvaJZr<4G>rEtr@`{sdHCCU5l zG-ib(SJ4HPMzwbDVY0T~sR8#b15>*u_WgDA2X!snc-wnUu5fBq4SbpjAY20Ugfq!2%xNTc$Vy2hbfR+o z=<9(Fuzo&po1f1Zp$#I8b_AzIde2{3OV2&Sz<2o?~DgQ|+MWfqKifu9JCes0M^27S;I? zkHWm2Z{JL=uiYF28(h>&vu=;3j24${7eaN2p}4JUBmJo=$$ILv2`X_uO522?qUJ(x ze1+kkyyu)#{c=Wm3i(8Z!30SkMin^t?5S5uIzxtB>-vMa(giz%zBp2&W1O6SA#0%0 z2}e3-{8|!IU0j21_pG?R${;TH4AuLFQ3Y(DZIV5p2A$4y!eA_FD$S#V%5j;B6rdvd zQdLmmWnihQPM@a4rnH7h>`Vi;iK{P4YUAglli2?J57@UVqR7$;#zg@a8+8U%R<@rG zV*fkKHGfdN{~PhS?MkecsSk~3$py-AD9;Pub&mRi{*)|0DYm96xiL{PKa?9z<|A&y za}Up(A}=67`i9QEQ_&r{gt!6c%L?r)H4O%Jie*bl4z~$sfXT|y=lX-=^)Dw?myf0Z%iP;5KvN9fC%pC0ZvKR?bY;C#CHgPWgU)=N{~<+GHJr=$oS zW8mza^6{V5F-_(peSx^eWt-ug@YTK-pnUdu!j$cD1%=z;3}P+O-}mG%;G zm{vm6FolSUhH?~)%Kuz0{}hhqd*@w2L}epOp|QIC(8YKId2A!?&eyee@W2&*E4}o2 z_nu;l(jGILS6;*^|Ix)XQ)S-tzf-0afSa+b6TyS-W*VFEQ9oJzA-H#hl4>D|Tz^(M z7a=zJYRcpxmf3iaj-Pf(ij-25;@xaA7xwVPaAgl{eKx$@AAb(L<4!3i+zD~0pjQs4 zUu9uUOR;mOy5)bG@tKxQbL((Qk$F*32X5_Ksz#Z)6#{io^xgiYPYBVIm(jp|pS@B1 zvb~*bn+j4?(&W_GYy*$J;m{HOKubAk+jS0R`@~b*<$|9OKi(Af0=B=kuuU^1LL#34 z`r6q$Q$bX`M^1Npl;>*9>NOzX{(jPPq|eHn&mgd*rt&qEvf&HA0@M5aMBH_3Eb-RuEy?yh z=Sjg3n!Pgr`p|q|xTx@=LR;yhD0Oo9vJQZ(W_jtUlHS%~=?!xmZL>u175$S^iUD`j zkOODr6{iJc)-&C^b$eH0)5Ym!XZL+qAz>W zy2KR-7=m3R){igc#VnXmOsov>*t$pS6>7i=f>T_e*9Iv;YFPvbCe5FG$OAP4uQSau zs8f)vTe)9JC(DQ+5<~7`k}KH=@YqjHQgpX=rJMcISm-aa=TOoEVqgSRlZs^6^d-HJ zL~h-n__RC_X|D*hgW7rEl~}R})Zo84g=I($o<2O!9{Z3-$b)OBJ_@8osgG&vR?m%a zei2Zf`@}L3++I{DXyr*04|Hk1CF>sxl#2WLj^0JdZC?r9VHr5-sBi8R-C8s0&71;7 zZ28EpFRgK|&9Gfo-NazUx3@PSCt@k}PFx!&mu9>hx6MoGBgHZytjDgZ(Czby9^}52 ztFcJ|n}R2bB^AA|ME@i=eE?Hyx2jm`5Z)Lk7nSh)qLZg3M)LJ!4NN`4`Bm_y@GCg~ z&aaxEX!s2)YMCY8rW+?K&@JTc$N|702=l@v2zh4@&%@Er9Y&B0BkvE1 zo8c{8uN6ue%kmx@UWv94-|yy%bhVsEsL$ygH>}p&e+}uW+D=U!O0!n_k&`_~xd1^E z);Tn0EB!V-n!n=I>L{ACFMzx3-rIpl*}n)y#FGuvZj&$kKWRzUO%+wmbr-BXa2`nN`^3w0E)JYAzoqrAwpy$X1;~i%Z?vheey98*=4V)XerHO?Gen z)`}VJ4(Q~6INX$??U{~Or7~xdg<5Crd(vU-VP-bdsAEZaK4~gGF%sUJ0z7OzH33-2 zgm}Zc`>GAj7AZ~d>PU}_9HU*=c`07`CaM;kV+0}1-Nt*Yi(@)cU465Bjqtnk-QO46 zy%pAOL>_rAY|HlXg6;*a+)x~(d^?JNq|Z`wT{sM-5YKY7O!2h$)Jqic)1;#OQvF(K zk#Eh#NeuGhLdQX|seRKoB2{l|*nCWU?q&pOns?cVj`P^kanMc=!T0;-Bh$l#W*f4u znoRN046XMkOO=*YdT%t0kmC~mioE>z1XCx%^Ol(SG0O%j?| zHD?)ifg<1g6d6{MjlOsDTtZo~z$6P%_J@vT)vSURk{oC1bpoAuu0)j%__Q)8Qa(}R zxTE`4Spa8I`xpOT{i{KqgH3?HWaaDyxN!r9Mb^DiG4Z1LY2IWU&xMLTsp=>yWqBYowLhMXBh?Uma$lnpRQfnAE+BsDB$tyEakk7LZaaP=t5y{J9Cd;*TZb*$YRt0Lpu2e zwooDpG^ATqxs1h2a4VaMR_6A!D$t`=>0a}gn^(M|$$sKHw0A5qep{inXPOKx8`1NI zHn11-^ezG9-r(0cZ$ipke;i1hoQKE99^k9<@mj})jjeVaIlxlR`PWI)Xa)|n49+OO zM>eJXNAHskz#*2ODTuqlpLbpf}|;Z_5fiVL%OCwOg_ z53276J8Olc`NXm@ae#_Sl~$Uku4}`h7n{nlpGr*Ylg#iZK5c#)4hs&^y*jVX5I-g% zd*9s2mJerJ#aI}L?W6i__p%B&at}nDRWY%SbjOsn)m3zghzLVgx_oW8!f7zU722@uwx$A5 ze)tsqfn|~sdE(~Wq$zdMfEsKC4}rF13?8ABn3Bh^anMxq#==!NM@-;3vkSK=7%;(U zXHxi#&(Q@oDNGdO3YnfRUu(Y8^aZ=?(>o-3r;_EKBPn1mh{d7wkdbKRH*~6(Zz58-i9ksZYD9jgYV|1?yCh zHQZ*ZssN9TLG^vs6mkt)?PfEE^=Ka=aXxn4ch0f$#~XnTy_ApisM>Ab?OtJr^v26^G&C4>b4W*I z(mCL?TJ)24mqqorXl?hNKMtd4O^*%vmR|og=XVSftA%@6E>nlV`o>ZyXy@5#Ld#E; zcFC(@7)LD6QhM{5^2M|i-y--ds{%-+Rd+z0;F)T(>qGn7WjS*h!0~2VgynYFokv15 zALrAKt9^6dMAr?at==#|W_Y1Y@zVLJ%F_0tx0fgzNMjSCq?n5!1TJyOTy2WJ+2C^f z_U=|Dn1Wvl??{Ut?-3Rbyf%JB-ByEYBs5R@-q|t#YI!H92$UUldU0V0BsIIS}t))qLg$-ze2~d4zuJxIuBVEhOCTf}?8EX_T;8)R`LW)x&4o>lVP!S|(Y$w;11g)}pg+oGXQ)JbPLG5_jj+0vF)seJOpy%6j7 zr#Ghq;J(RFWM<1n761`~1 z*OGyw6-SlVs_gEZgqr)EUV(+Yxm5_*Hv7mY=xGBWmopZ6!_>Si850W+bdHQ1R za-U9%3CQE2F32fk^2M@R7DuSfR)2LjeU-1hM_cltj~$L|;5)Qj(-ho(SOz z>8~*B8D$9FMZ~E5a8)vSP8us+&-P$jVoivwzPJ94@!gz@PMC68!(8(n>34U}1TzPV zZwbatI=E{^Pa+4fbNqC8wW2|PrJ|Abd*P_NKge0PBi(g+sH=q41Y@YntUo2%j)N4b z&B}md=!rJbX-#vNRv{Fcgw4agT~W3K;7aVqtuGIS=`<`9)>Crw6k!<=AQUd#MxHZI zQ*3cAp{0?*+wAi`9w!;AuMv8f|MMzh< z__wP~X3peoH&9u+SP>7L;2OwLZT5WaD6ignb*re=02cbm%-YUj0G|@E`nVZhMW=;O zu(S{fXY-5pivGRX_OEK_;SoZgElWW?%PqPZG|fAFoaXS@#BxJ=Kr2^@~-U zP}GYhd8n3mEP6NY$g4R2Ot3#Zzb9!{6`}2safDk@N_b++#)zB;LFY{x6gD~450Q+d z9{kaX0CUdQY)n;aafdW)OYC0JV?iu^N#4@2UytR$$6$|=<{P&Fz>R`E!C0}(PxY2( zA*b?$sgKQ?le%YZ-68`O?d|P3+h(1O%Ta)9Q0`q}mPQY-#)Qb|EQIH~4_E^s|8OTd z?UdjBjiC;qQ!WJTuAH}JW9?=ori5P-QZ`>#j|${iNI}o(NiU3#EjP^J|K4N) z0j|K=R;gNvrzepFgo-U|Tg+y_tJNk<@WY&)j8CBK&M@I7mwq8M$X>MaPS5&K1+lWQ za~YDK{?eQ758|#QftuPO7h$tbqM5rrc^74FhJy-eWF%0k=aJ-3vA-l+Q8-2lHsU0@mKMN{S-& zszOgAibo>RWfN9}rtGmthBK5PG{^qojp1|}*1(??ioyrcI21Gm{Y-Y;fFMsE_9|P0 z&eD)(T8!-%f^gUsj2wLW58drL~ZuPvLhifF1>-IQlZCIyy&SOTcu@a6PaVX8%ejE4Zv>ns%gG#`MX%|1L$5<%Nz3531|#z zu&>!rq--Am3=0jMdhCdKBOXPd4XEcM z1t-8D4?~#S(&|;I_O;lb0T!wvI^|yez}3)6Z_1s&N?H; zVHhX#5@$Rm-5` z-yuucvFg%v4!hyX@12k~^3pOfl#g!xW*WA|E`b1uKR+U&nY6>j9+(rrA$9@-4aM6E z{te(A6r=Gu6gL)A7j6T%eY=8tjt_x7a7HnE^ph93{Uoh?{SkrnO^Qb*iR6uZTmR$Y z5-u`hWWKQknQQi9ojw}JcB|5+CyJJv0L)w|KH#aN_BfVd1@*n?e>k`Zke0>mUzR>o z;i2R2;IPFGVcSp38@?r!AJ_A)zvqFRz$@0&(iR?j=pEi=-;rvfGH;XDN-v%xlJp$Q zgss1&FsK9!{$2wL$cW8{^O4t~qK=bLxj6q!?~0N^#~qnzg!Zwq%6#G|?|QzKT<$lb z6q1Zi0q}`c$t3&si3<}7(~h>1*Ywh|lD*zho~8$Gfn*(tyGuP%$ahQPe>xg}K0V!w zN3-oj-WRBlHv2|l$w!$47|B_^HNO_{LBdSfC30fMy~BwB$l{TW3Mx*|s#j3tO@mIj z0b?kSBeotB<-ZVY_l6Tza4X>uSNrrgVm2q`BY7~p!+l9X#kiMqw&+(KULMHcm|U86 z5uJdkjz`>-bF`$gyPc)OY*T^g1}+TeMpYb81s0_4Ys=nAT02SZ>eM=)F>3c>h%ksY zE3BQ+0mcisK1biO37NE6+n5^&jVW~U2l_RQ?e2CHwhpRCSD0gG<~55F?AOe>Lc)$n z1JNFZ5j3>Qf&8ZHuvn4ZX5)OYoiIN-UbDfuvCC%ZLu5(BIH|J0mYL7!2jL}o=cUan zi6EaIAB;|!B8!6J1t^D3`fE(wbc z6t><&Rs))1qGlUF5xafJy6t7Yba_P%^R9L8jYpGhAlJRfS}Me%?Y?6w>nDr7^**C5LZW-e z3}#Oj7xuKuRD^K1VN@Mo8-e972kA=6u&Tc3{;krEXZ1?fS$5d?#hmPTUeF6j~x z0YQ;2=^muJHv`DfokMrSP{Yjp?{S}f9^H?eXTR^~^KI6enU&YN*6+$iNvb}d4b4on zd#7$aiI{utXs0=m#J$+p5dikwo-W({o%iC)O2F>u!dyRHmJHlQ3AP_cZCl||PhBy7 zhcGw>tp@1gE1z0Wxc*^SReQaXLgQ4V!(!qQv$NG=pmgeDI>EC`Ei`YSdV6cPCa1w6 z#~sq_(Z*;#XfVy8>41>1ftv&c6e-z!IOQDRVz+M)!}darHV;^9*Na+p2E64+8Ev zs$(TY({zq`?4R&ILuAo=k!f-_hUCTp}1Oj zQmE*0+hu`0F6ZbYMy= ze7w=3e1imB+*uaPaQ^c~`ADYs_w}6!wiV<9z?nhiy3#?xyogw}(?*Kzk!AUD<)|l8 z7s@%+fwrzg=+YUiEntx>%G?osdc62aRLBHV91JdOB}7cMy_Farr6p}6r3OHj z;hwxG%7ld9LbTw4_3u#5Djfhhb@E>|T0ZbkCS3RW#=qJ&m+}nmg-Kxb&1X%Fi~!;* z;nwBF4IM=`Grr5V?;(7LI}B1WfNkNHrM=g)&#;DtN@%Qn{pcy*5d}(#)M~w(BZsGhhX!WdUOo2)jwH!CjH{;AQ`I9LE^e zr3R4z$#GHSIvL$?xZAzPY^TWhf^`diEV-MKL)Gk2$4k3Msb0a|D&P?3P=?!w$C()z z@9@)%UZg(nYbW%ERR(;;SjuvK%SVD_d>CB|;w{8HCcF(cJ})4kv-E?#X3uZcAAPNP z{2S-9my{PuL3MYb6Q}y);&a`D!7ZwaH#HwtqWf>_7Lj#gJ!vY`C4$w5je-mF6B>*F z`MtH3eg7?B=og3!xb1!~alOS$aoLZKHq=Ja%r*ouA44)xiMqA~@k=)a?=>;)L~49ovev{)}0+fpg4KsQR0 zL!~)R1tygjju5LVX93x$9OC^!nN<7w*eO+_fmZFk7qyQ@> zIOL@D*nfPlPS<4WivJGf*yw(FQTSk+Vpheev)j6XoWRb01*s!zZ~-`2n}lh&>xS@f z37H6M+XNZ^;xbvMC-NW=Sq8{eaj~(x@&q59r-^@quEeAOS4E-cg}&o2h$K45p&fUn zrVa`F^T!`$;|+}58=mSFOQ?Sgz8BS~T3%yHsKQ?J<~@>%IOqF%f1g}rV0ayG>sYHU zpwFrcqXroQ&X|#ass|VAx;p`$qvWi9n*JR+Ze2T^hn3E&F{glauia>=71=h8$SpXTK%P z;cn34JGkyf>{C$!8$Xyo!D%Vc9NosO6&IUD(i4Phm-bZ zM!*zOe? zz&RwkIyy7xnAKjrmgJm0K+e$+7j>}*7c2@Yw!^*bgnkL^`O7OCdi7uA{yjugjgw+r z`O@5)0CX8n6U|{z#edp{drh)zSl6wXHBmQ|9EXBfvJiF#3|? ze`@i_gJ_(G>KIk2whAzFNziCPsmr0x(v;BZnBzGDqpA6>HPdk>euz$+nz*)eXD4{l z(cXo1-%u07h@Cc=Tg%0pGtx5%$o2Pm)J*!Y+G#N=Jrg4$*70u)lW*O+m;{^UD6uFTS2R52{# zPclaVXeQQ=1SXsDKwdm@O=5Gra-qUNo(H*}Pz~;rf8k{LTh(b+92Slg=;N0Iwp>-f z-G+3m@_Gc5;sK#wItp;SU#DZkkdY5tS2RL**6(n02f_HAz>%aPRBKJK6Ld=K#Yy&l z8GB3JoCa{O;Wmo{(Q$C~6Cd>8u$CG6_&Yqs1BJs!eS|(-D~K(JdvY?>M8v42i4E%l z$PFF5*(V34PBISN5Tz&|`I-TXzU!Bqn^&ZPzQTQ=5J?6D9MDXMN^ue9d4*uDBSFAZ z-ANTqcTM!+LyBR9S}sUuwxjc0CF6ADRp%-F#AAca!uXKaOvQm6d9zlZe72n7Zh|LW zZ7d5`^i-qh?W|8=z{yIx|8oJFd*;UXIZ4dn&Qq`-G0&q8#H*?Nk9M|zX7qul_YBX8 z-=@_h;1V$Ubbo3_MD=v_@Hx<#fKn%7Nlq#e267+K-Za=}k`ErS#!Sw|$)cf(yszh1 zuIzrfP~MiuOFJ?eJ|DYYx;VBYOHe+^9vtO}K>eSZvmlvL05ShiUn5$9d~Oe_*N` zMG-5C&hK!_trZC5^omna)#O#3XL%^d=UzQH7VPb9e>FGQu#s<(wc@;naGZ9OcX z|4OVtwIy0Qn($r-=hcUqYUeF0wFL-ac|OE{r%2bMb;PKMF=hnHx;sa(;JM%QS1te- zU+)RbXEztD&#wE{ny?^aSLIY1pB3$I^*pP9OSt=#d9cSHCNzS~RV;w&0)Rph*P)cL z{K&F#>qp%pYJ$wJb<8^*_)EHGWuvauNA}6Qp0BIxJ-Xo==5j(aTwW@R9w$2qfPgB_ zWnLp{|E|8j$^Q}=egz;|Oq@Hibc&k4Ge?apA7~uOsM5ezoHgONnTMxDh{({^Vy zkl_$`4n$Ul>mj+?4KoU;w7VDm?aEP`=|;CW$smdtC@5l1musJ>7{J+W<0(PcOFO)+ z(}TQZ+ve2Nv>g$?)!$j@v93t_%|YVCfbR;s{r7ehSsaOQP5JRoD|Mh_g!?vfUNTTtk)o~q#4q&FydV<+$F528Ehl41!_(m+{Y9d$TPHmn*h%v1f&|P-g zZ3eFd7ikUR*@C0W5PvFz;iSKf2UsS0t3}2BXfU`kg2oF1W+}_2fCi~+L#fhHfTNL- z*W0k0p%Do5?kwQ*^18X;P)%~n?Le!4;!U$h1R>Cd@3)I zXZJ!Pbvqr0bwBB7AM6!))fQ9p-QkaPKl+3mzi69bd=SBap0Y)HfGZbN+bs6f+Jw)? zuuge3eXF1J(v^Mfo?M44G&)c1ED8ZUavmIbqmAPdJd1<8qK^PQod@7{ly58~?co7@ z_sRo0VpRVaHz4|!S7~?+YgL;^I^ZS63t0K{OlWw1^346Wx32OJf-~JG>uw2E1dNlY zrXH*h`V`@M;wLO*T|AnT3modmTW?JWb7Rr3N*0xVcNtHbg)xz>^s>O*&AlMqbt=+p zRb`J1O0FYLLccnUPpkphh9lsn`x7mewW019&T%3B69qy0BU~$$Qzk$iqHV9Fuj(@T zP!bk3+A9B@jzN3ItqPpkHINQ-pbLGUZ-|9gFi+%dD((mzj6o=S{J8Gc zktS!);&)cGh2U7mW^_v(vtk9CihT8gIJw1ttA+0^yC2Hi1#GW~=YY>n&E($7CyIMi;MpS5AEU%u{q(V2x|9 zFkw`*_PSt_KK#=QG+LV0uls*=N&oZ7^?WiVG)Sh9Bj*Kl0ze_d&MXIEexY0ex9tqS zQ~)B4*$F9McFKK{a{u1Vzy$W%ecwvkk1AZJ`%U+S{PNRm&)$7PELF*W-WO;+Qy?-xK|T)f zZI(oB1J2(5!AUQA_cUsU0?UwIuvqSnu1%4edEuM&9w)Ujk-PJA-O$7Qd30MK17*^Y z6#Z=h`<}{PLuo6Psg^H+D(J0+`sla-__S(-M+EKj3HSc@?$tMnheZUVCR?q)g?PB` z@Ehx1p-io|+`aD2ap39Cd=j;@L5I9O&qG-opxn;Q1ozn}8^7zh>T^BL>u3hK__?Et z*NY0FJV6O4IX_p}F0IGu!;AF|)~(>pBpc>i+_$bAcIHR9`6rDwdBnVYTF3cYmERyX zI;J++6_1fc$2{LL7$$bFSlsJml;MCQXpUKA)66Mji+gC*E%|zdbdphh)&AhECm-vFG}fewVX$bCPWRLHr})O=wi^;} zEH5h^L-H!28sG&LRgPT=5*K%K4=|X0k(h++-Nzeg+1zsE#08PT)QO@eb`!OOfMg!oM&32 zvs<+SNZ|`nno|@=&ufz%S=rfvDI5fq&5X$n7H^CA( zTWbuqHj&CKYe<5i3^C-Frshz zU6{`g##{0dXj;=`mfsSO3xA#?7i*I;4zxBf{;tj}qI_c8v_o)4n6Q{S@XwBGy}uE| z{~Y&Me1QJozTw^3bYCCz)Wc&I%LO+gFNgxsQ1;6+wVgu9Bh}xDHGmc?s~P~`j=Tn> z|67UrXLml4SfxxyJGb!%AWkf}0_BPIvR(>RGpIXVy~H^`@puu?$#ZGY^8G&Wvr`ru zP#>w#MfksUX8#)fUBIW)`kAc$U2n1bJynk8#j82FG5(h*W3B6sZ;=0fU?%5)HK958 z>B>)g@az57sS0!qj?<94d}hb|*toa&qV~#P|M^psTqVHr5VO7z`u!-d6Sxk%d=a$y(tbY0D&u+?tfKfp={|@p@^19P= z0o%y=M*)AX>TihU0(M38l%BT!PWSf1!&c)nFkM#0@p$In=pud$M@$>otb$ysjWgT0 z^6D}$5Ox9wlE9xQ@bk>B1OwePrhWK$W{!puL+oL(JK%RdYvFzDfL@}=^#4NyyQ~;6 zU-~e-s(+)MzwdL8A(r%tvXc#d9^S7%AHc4>d86Q21Hr-T14fc5c@%usbiKvM056of zi}e0%zl**5F(@u-V8U@SOw0cvNVWwSEW1_n87CRL?GJ;O1b-d<&odS~2Mm-j?0(hz zvqt)D1ZYCnV*e)9S!d?l4-f2ui4T!yt{5veg+^qQeb`gfXkoT7=K=;Usfjz;1LB< z(t2la{|z~yn|Hp*?&KCAmmuk#X(N|A2^Xa&c``V7BGDF43PyY&I_+RuG`xV$3SrDVcnM-!g2YVHnUi}XlFJ59j zcJlGJ&g|ui2{w9)7kH?R<1BLap}zw(LiX&u_L(#y_B9?D$->)_kG~w0pH`^nGj>(m zN0%nf+#6ruQWS|+?o~K*AT7W^PW8o>!p`2A0Tp2Bt3LGpCam?((SLn_rNqxgot0V}IeSq2l1>V8hil9v?hClb`+HH+&~n)ejDTA@@@Xt6%9v z?6G+>K)Gv|WMc>?Tj$-}dgn#6_!d)LVr7yrB9#UJ}AR*ap@XhtNN#F?}CKf%`p z6X0+bu*$ffwPrqH0Ev+DyY1ilb&mhj<<}PMsnJ*L3_Nqb0}{ZJxqdA?b1vu3hp+;h zXAoq6CV=w^1Fk$$M$P@-dDH(G&DRzH0l@W@x&zMyU&>e#!GFeeHV@FV1s0^fg0o@g z%p(z7gyjMJg8Rg0Ey+2b8sI1HHa6=kj0p~kYW{Jb-;c55J8 zQxFVLW^$8CtP@DO)QxcWSb5ABAY^$@#L?G8134Y~3$p8LEV zF0F4oH^_wUEuC?Q_e4Vg@VCojfH$T(-5uWTyKU+eUz(2cE}CC4xq80i;MI^!B3Gqk zytzI9b2|zh`gUsmOY(PWZrdmLM{%xjxLjhlo)3=qXSkvhTG`d6{^tA)B6CVF%KCn{J?7Cj@i%rE z-Z?iv$1~zGzhyA~sX9_J9VBAjbOTT0?Kxh*W@%3TUlu4@cv0h@45}eSQdT=*WL~CK zg_DWDe_$|Bw#z|_JM}ZCtak0lyll5B^CSNL&x1kIUHcZL_rf_@)oUx}WqztID*JOQ z#DcC&gKgfJ(*JuCrMjBPE)=(Fna?bQ#D6N@#%o&R82MIfnbZU0*eZd-_Lz^kU}d|E>|DyYnw3#ANqS?AB3Ik?n8bWlL%WOzxs*}yKUJ#_ z0eKYuwP6#NN!UAo-H_J@(dPezp3d=7a63BchjZRi>&%*$QBq^z;qSK{41UqY<%rM^ z=M~n>Z=08qR%K`7?=K$wAk$T1h?xD%`BQbrm?BjY_iN(wtm58rjWg*nzs?J+qU{jywb2ho2{qUZWFG$jKfXJMBymqQ&9Z@Rkr2feWXBvzq4CIg~lv72F> zOCA$%+P>QIz}U4+<5PP~WS)+e-OttPnnZKO^m&iJN?x$x0bQW-b+a}r`o-ZVkKlEqKd;#$}q_j|JVnOcl28H2lHzY+LQ+->Nb-tLq6 zb?)HM%AuwXnqLpnU#!Q_P3F%@m=M_*DEvYeKT``kkINZk@l$ni9*{-@%M^O<;Z;!I z)QSBccJkwxtaA)&ym6uXg)%PuMJg7de@+5N7RQRk>=&~5nOa)ttwzLcgug7$bXRt0NnnIL;!IA3uyf>5`j7X0$TrzME)zz+&NR6p-}A- zaNf{!wXCXAA8}25(aPW<(g};_8AoGoMjUB8W(*$w{DAGeKOp(N8cAm?cwh`DIsB1Q zT}0Jf3#IdUjK-NZ@HX|=Q5kiSHQb2HM#!gKMf9>BotR?hsT~DXQ)E5vTZ;3~?_A`5 zJC*Bj^$*_^UqzO$DI-_ZJm9SLlY`D~#d+geZW5At4=JuU`hI=rE0*SbO#tVb#x-9Y z&!wBuuOIEnWXv-j-YPH_eIzfu6uTy9jNx1Qj`F5<7EeBDU>P9WG;RnMOn&L1N3nWw ziL-Q-UNL-#TDd^MQXs;n&nf%q0cC&k={+VUmgJ&21`BI=Ho?_k=kMwzmk6T;y<7Aa zy;@TmXt2Sm2 ztwo=Vt1a|b73?m1k3|*TrDFHL?$Cr2cdJ4FTj#r~o}1B?dPSYu-Z6hK4BV`!G%N|3 z^o|JuR}EZ`{(V7ULRrd*NqaBz(#=i(ESqkHSab*f$3p{S!VA#x75Xt>+)qGHHin@U zlQ`gs_fLj!!JmX@od~0C>8`1hMoS*NpSv{;{q&+Gwjod7i%$oi^P@7;5FVHu*}TIP zZJVZ8NDs;#p_Nk$rx{FdBy)WkCn9?*Ld8An3q{RmiMYDS852<_o{xGp305hre0SP# zSO_lpI!ZA``@{$l&ND*!a5-H_+LVI}=|gFLty2oNr4LZA=Sawgt0bKIUUIs%vo&5A zKEyQ&hYh%irlt6`p0-XnUkP54yKs^di~H(w(#Yo!JfYB>vx{CQW zTi3%z{IbB|f~eZrOuy5OP7gZ{%Oh|#CK^fNyEAd|v95|e_Efz0{V`l@%b@TjZ!vw}wXa_K^-LSasdjtP|UC*zF4I31 zdt6!KFytywa+t-sAvk%#cSlYz_+aLo65%&0*mW8FqK3Tl_R%`$IWzQ_!tlX=?F89l ziV8{4`xzsU!G#CD_d!8=SDHKYpjTetJ=ddXyz?%qG5GEm&}S()@0VCdGegMcq{g!( z(Q+}jzB`!%D{sAxXZh+@>+UKuRA!hOE{&nM_I!?UhM8-i)XtMVX~IEbnHnllVDerx|He1ehF{Fz3RS)L zDk^7+YCXJTYCWrZZ$$sTATT;M$sUT}O*%yBoSbwukA4={y~ewv@r@{}=;K_aXY!^^gg z9cD4I$H#8k8)ONMI4eK;c>GWMXwW;&=`QPefuQ|Vz~#Om%3e`RYLs{px^@w@QXfwT zy{Q~b=|?1<`yR{92(SIZxz6ThEFh|S`$zUohX3 zVeLFJR2NR1tX~tpnPSP;FamI4&Ua)|H=_MH-tCxz3@$SSbAc`yT%POKgHp-i_ci4I z?j-%f)9$E0XDl`CON|2FebqZ%!f8BGVxme4`wB_A*w$(*v^=&sQ1mpTKD3p-ZUesb z|Lfpf1_RfAUs#}UcJo&jx)Iu7xa0gjWzXJX+F~z4xoT>b&#J$i-|~E=G?+k6>={ z3q8VQ`)KpaWO1Ov3Mn-1!w!zWkvl$k_GrK!p*^VOs?l%QPY56n%$*_)u#zWQC(2FI z*+#4kOD(OW|BPB1S@@{xk0wh?T-xPSdKSMhT~E^MFg^@Y2BCe68;`ONJ)cPuh6aKz zkk3i#`L;35pQG?wXvF)$!m;dHkG>lx<^#vjye;l8buJ$`x(q>=NT5rL>&b(<7U|8$ zlECo+C)|j*1Z8*kc#wbUz!`B%^tC3f9QWG~J97L%PBZ81vAp|cn7x|OLaku-ygyMI&;+3bu zasAeT!h204cHQM`!|^9NgYEEE7$HSZS1IzeH69BZWC+J{QhtC2@_)K;#f~?KYciO* zx8kkv3Xn{p`(7;p((Q?^J5Q)bvj+^`Xky`n2TM}zCp)n96$L|r#y8aU23c=$%dm)a zoYk@BG01*Ge_Ze!=idwIk8SK*z?y-Vq1d@o z^pJd(9|C_E=&C)G80%+AxWJKCMo3|XIf>jF=eY;PKuG13MnA?NkSwmY4!5&(?rg3J|l{~ygq|9@zn zJjp`Ueqwz2sPK_j%NscFj!IZ4f)6+VU{5yIgwjNk4dGSu4;u_j&|*p4*ES9606DJb z)ytKTfaNMyE#mYR|6V91v6ACo)2h~6j4zify$t?+!Kv)#&0sGCT7KPPs)SsN!*i$F z+;m6K++wRW5G$uNsgztKYyzladV9*I_B>dt{p~P5c&2v76(3gd`C`E(7;v{A6IJ|r z=NAqUfQRaAL@t5jS6-Rpa+>Q_BoG5&GO;p}hjsLIE^J!Q9l%YTp@~VFCg!4YT<`R1 z$RH`5(YKzxG)k0P*_9j7!Xh(>x~1k*J#btEgc}e1_k#J;X>e`=go^+S8L=>D(EEKs zYefz~6o=V*HEgDHFR%(hyh!hw*Z(L1`A-s;uBiPjr%_^x1?hu<$mhJXwER4W-sNG z7CHGSgzN&e`qZ!O&pY|nO6l|Pj4zR6VL2)Va!*NUQ#J(4dIQNAW>=ZHxkDcHab`Ph zj43NSd~wf)qRajJ%g`INTmAfP3%S zmG_M~=BBq%6p#V1_R2qlwU44JuLci4Mqc!)9@r40ectdf#1 z2b+@;Pe0!Lm`n9yvg-N3vr-k&N|A_!hp8y!L2sjkzp|>TYI5BTZnKv!Ufkfe80p!s z_QHq=I4tW;>>ZOkEjG6BSdJ+;b*MVLI z92ZG(C_HPu_Uf3>6ykooZTXFw6Ivl7uAM^Si$d~UVeYcG4&&A9^`W# zWXC)n1O#(x-QzC+^ssl>?!rJv7hblo0DYhkFl>IGJ~oBbmRoNpjO{#-hMsV>x0bim zY%q4erizWI*kzBNbF0>L51NJS-J7B0`om);Pw+9yLJ%Gb zC53k?-pYOFWmjm1ISWCD1Th$U`C>0xtd#qR+#s)gj^LL%tmNCSH#9TBvYMJxnaLx| z<4f5sq{J}=KCTK2VW+A$M|o;+8E~)2Z)z$k3NtaXWz5dr(@djJ%P?9_zo^Dr?YC1U zj*RXd?i=A44x3_UWaNNn$|Y2R#XTB*yG_UTvlY~`X?fQ5t5%?zD(SIO`^}`SvZaUf z^M>44<>}`|5G}^f&xco)ZWNf z!`GSea3)~YtouW>Rp(PX@WPj({qC@0L86B;4fjC&k?me~(9n7(T!OaKGpX)qmXnEv z<>9c*lAQKR4s)wQcVg`zDlntqt=UE8r=6*5Jay3+#@a))An<(IN4xIl0zE4G3~*cC z1krtQSIK-?*vWQn)|3?UilE)#hZa4EVbj|A;@{l{*{>)ofK*JaxqAOW4Rb9gd+Ghb zfcX=_7ca79MZY9|-I~&r*0gsDw?>acx*I<1_`7}VYf@8t3E2!d2JQuXY*ND1B-H*e zc-PaV>k;r8m|xY}bt5oOa^n&Scfawx@XSCOIv;AZkahMSk6loq1m zw4#M=S7%bUXvG~ro5z%04G!(r*G@)3X{?eHg~~=f;Cr5)Ay!9Ba-aPfgd=m8Ygtlu1Q{8iQ|N1LC&AKNBAbiW~IY6&g7-PG(%k z3hCiaJHjeQb*Kgw1bv?waOZ9us7`#HB%h;o+gCTsrtfsbj|l*h$Bfk&rCxnm@9A;U ziSptJUupdq(UagTn}SQoG?`S6 z11N6QMl|hr&zA)ula}WXSqJoeAtLpvD!J@}#bMLtVwjdE#TlJ27t}4%TLR0~*AM!I zQqQ9@I^2$Va=nfTdH)r|%qFy>n2&k25pB9|YIakvfspH=#xdn7@7A6Z&K6XjxSI7n=iAm-n4zrpJyRBF=u8JwH5W8LlEkKq}{!R<=l8J>F98CU*NWd@!> z3a(vazuzxTo?p3=F`lIh$6z4uL{>(&X(R5vWH*mjoyyd{dN##sYyZQI78QvdhMK`N0jmC`o#KP*VT^oEO%t$0@&i^6IMJ}O4*0b5Irb?MT_7z)=yia*q>$YQd9TdNm}r^*dv3U&6xln3~qw z_Z#w^x9axCG05VJW|%!)QmH)Hqm6~~hv|=VVzSVaTVCWN*VEI}V{*2$&6-?$%bBRq z<;9KL9A=-ZvvacSU*h*6auxi_d37-j%2hF_9m4%T!Z8=?ZrbHZY!5GgttHCzI$og6 zn01e0n|xTQ<8d;Vw51ll@f5~c^+9gD>=w+>6W~nC0koc9H^$3*ffw-xnXT(N2RHp_ ztn(h>!7KiL zywuXKx9GnXu|@`CT1rxr_lr~OYK5~$w`?V-$@|9qz7Qv`gV}!YWvn7`rLXNkMtnFdMr%%tvckSj}b?OI8-Z6buRCeHkEaB4TWG|XKDnwsmuXszrrX^NT ze@wKnV2OZ#24D%Vd`%~e6bO^lirqOdr(fQzziYA-`8-@U%yVl>c)w{NbEVq5ay~8X zzETW2_#>@uh*9L#XUpN2z=3hG_EDsE=IaYnipX z=U2*$RZG&dzas?F3gCFvswxe`noC{UDth(QoiTzz6rQN`7*6XfqNWnl$;en9OUPn@ zW-kAxp};pU(FG<_)m(9|seA8vmT=eO5D2HIN=Fe>@ga- z2W6JsoRWqITks_>lXa4~u>kK2~lnZ>fSn z=Pk0Tc#F#7A00-Ep6jtcsdUj)E{y5%rwRHbK>=; z7QBBI-$(sP(5tC)RxE|e4~bw*B>@YPAcMekaRoiqWMLL|UutO<@(<^Wi8fx_7h*=( z22lDDx%s(^ZnyHNXsLqPMU-D$(rHhYg>UN3N$}mR(vuF1rDSDeBXX9OSt3*He&WbN`5LU85w6?*;S-?(!pLQnVlmJ!{+_e(Y&$AynJyj#=2Wo#nP#AV%*>%GL+Q zr&54)*wwB+RV%D~u2Nnv#_GN7=4j-ef~PbCZu!12@WOu#A#C0Grc;@K=v2Hjw)86l ztyiN-mF+B_z>RNrQZb^>}RgRh5^XGxb^6V4w%7e>}3&~4~Uib7D5*q5h(XIlL zq=fK{CjqR2jA)P$oSbMQiVL2&=)a0v{8IjYJPz;0FW=4aig!Jq@tx=V-Dex&D&#_; zr>PbGoMS+!EQ{`)7IxoJk|;ogHL)9KRmv*JfkOP&tGuOc()Ts35@n*Yb9QKWAgO9P zF)z%A-d##(=+Rc%Ob7B;#V^8+--B!R1XjG=r!ypG3;<7C-|23lLp~40;AulgBr==n zJ_L7RiC84*2Mk0EnU@$Cw~)_iW``j=dEt!@5;b zC(Yiuts$9mX4-S}?Mh~zb+1a*rBP}U$F;FjZ|2C!YKHe#M`p45f}P{V1tq=3bmR_# z82tAFLTP%D+h+txSUe)UcXAQ57b~`o@>5?=bV1=oyU5--KsT~Db=SPn|Ca@+zf`mF z&E#lJc83X~rWjwb=4i8bACM%&q)*5TT#K~oAr4K%2ELF)k<(VX&|D^#TPA}!TPt-K zWaWbJi6I~f%et+|mO6ic)H$WO5Swjxl0oIuc7|oSR9qo(HY(dj_FAb@#Aew{>jOC| z<26vn>4ruJbk8N(F!$XAvcWYaQcV%cH+_`B!%O?;ThuNs#SLdC3c2_K>8Vu6!iFya z!k1_I@n+TcZa@Ujkk_7Eil6d;86uO=r!?P>J!9%kZ|T%~Hg(c}6Y**^R(@$Crm~a` zrr+tN5mGLdR?gSp?$A4R9G=ok9K3Y7Tt0Exy7*I0*&eLFAC}V4mky7`$J6euvz^Ou z==E>zq`h&9CaurdQjv7E*ug4_r?yI6XpN|>tcCR!XM5IDvUDhde-h7~7SHEr-Dxy$sb#0RW`1TDawEzA-XJ}j) z>LNlFuoNT_O^2%4udphsI84Hgle&p(EDjJt4yjVq8CfBahN(gWN^&Vt-P+31tFj(D z)Umllu+#14ov@|z@<+ZqwQdOR83cl6s5g=|ZaJE1s%XC2+H`L*gzFjpKx;_t$48HD z#FfXdeqVP@6xOcgFAd6JE5JS8@&bkLkDDN9ULuAv<-KBUCPM}?WCbMvT|IG4R+%Udau0LALZZeI4J^PKFHp#|2UpuZIglFsuM zT>N87G`eLoW?0@kW*AR@ix#GT2wP)lfSI2!JR}7dx-GVl0TQWSb&$#NI&q2H+)CxM z)Fqfhm9A!dtK|t#YL|qjf?cyf1|!}a>Z3qCz%h=JFo1*xJg|^=>xk9!7~^S>1Djvk zO4qcm7ui;<^w4Hqa;`rPin(5IxBp-Pa!f)%EEKwlL~O}^b6>Bxs!YK(Q?Mm%{Y5T( zt0G{Z*}##n=99owwR5?P9>^1r>{kjI&_+4mkck^T$z;N((_S+8alG(Y zxq`ZbqOEJC7NZ|LQ62Eu;Z}&&vG61&uHWnO%Uo5~Sdk4&vP9ke4e^s!QQGYQ%-V2) zd_8(_3qYW^Gj8R6*ul8Za{wZLYL3m`U17-n5s&t$MlLb=4g@1F-rzQ0Ls!2AIJ)TL)6GZS>*}o!%mcH(wQDF`taV_Js-#?<=Wz zprBq5LAf6_?gfq)=ZdL_4QvFX|NTX%73pQDwN^z?r2E&L;J7=d)Q_vK1rWDRvYE+F zvRTYwA}>IZ`R~n^W&t&YEz=Q%c}r*yW`O?YG~FC^xn2vut$_PbD1S4iU@<+v%t9sI z<8w~mRPGYL+0+S~OHIXybZV@`N}3n$AP7i7()e?<-1gt=%+~9GtM728nO$jc%1uE+ z#r=T<{-EQh=mmc16>V}7I3Q+k4Sh=j$RH%AuQrik*k9kF?4HA*xXZ>N&|G=sV&FLR8$hZK1Jr7t0BP*L$sf& ze4>?&(sfEF+U8A5m!=HBoAdC}`)1I}6Pv-bRiIPcXVQBY8E0M9sa)>0man!_OFE@$ z-5_j8G6Qj}aR#FpzzDNi?q9B5hbFNs8Z0QMW#kyU&S-%IgD=1>*@2 zxAgF^?GGAjg#xVvv6hyW{XL9xdGF0P%@u`ngMXkdFfp;EtU+~6*&I#afwzy5Ub0!3 zL`k4m=~EbEFOzm-!UfjNxR?fDSFk2bE(0iodN-S@|d z-NKl(6G)ti=9H>HUd|Ci_UhwS6%|^PtA<4c_GXRP>ki6ngk2hD9%hJE=@Mm6B;aJ% zD$q*OQw3g(b-giBV<&?4Y-I7=s&Tyr`+Z@;{gDM|it>H=ijw@IbM=MBqc@`;Z}7j9 z3ayLHQOlL;7DB#tkubH5uI~W~K zvnr_=*KZ6la_Kcx?hkNc(nCcPjeHk;4p;O}WXY`4eQ*hK%Rdb%Ti?!?s`9OZu!(L3 zb;}gSOuyLo>0Uzt21F711`+e2YOvgdGhA#CrxEV7mM~Bc7vsz<$gQ9ib{#mpz->9E<-frYPBKnNI47HhE~}QA z))nR5o1|XM&p(*~uU2Z(%VyM}2zr_dD0-rFJstyqeZYCjcyS0J;0p=!+MR#yr8^Qh z2_W~@|G<=?GZ$^<#P0GC>ruq%-rOYMB)P8Ne-2!D+%jVY`u7()lec)NwsJ39_Zt^~ zWTGm-`qCstCbK#^7IRWz`Z1^n+8~PxmzI^zaxLRy;mz75$~5NLCplE`rFhSMBplVL z{F*j3w=pT3iEPBA`*!V0x0maPj9ln#x9o-crI0!iv-wvljJ5QDj?~ZrKLHTgdlQAL zS7cQ#E{4mCl34WF!6}199ary%RGGbL6DAeeW=F&hTQM2xrZ644U$v`M?*|V^;!h&( zSCMS5>7BeOJqLPyDhs~mb&62)7YYx183L{o)?^h^&IHUvSgqw<@&k5OVX0wRs{9&$ z8i(O-)Z8bG;yBNStQU>4Q6f2WBu%~Z#+a$jMH*8ax|cG5$`qHh&hB+{%X?G1J-K_nk>!tTR~Z^)@iOf0HRi?Nx<}F=^SjR$q*iW= zcJ*bfK?i+Bqib?%@5>pl6wsT?x+Qd5KR*$LAADyQNj3-wMUpU|4(!#a+Sihc81}aW z2g|5Dxz7t3y214<`9@$MV9(+Ca!XBOH93f_<_Wn5wLC&44oGaPd!mmvzALM3(`ZvA z1y`!~Au7(z*?r9fU26h`Th;lw9O4-AfcF+VHRT)~51lLpqr)mlLb^}3g~uxG-QIUKkk#qA11e&fXS#=E6(Q6f z#M+Jvd@tnWc=iT>gmfe|GK`Fj8QovLD7(J!phT4^*UEuBPoMO`A7gyHNdP^$K!b=R z`AFsd=MIz6qOI>L!Pb3g+1W3>^#O&ryfJ%nIP+6Ny*AMqSy5Yno6Gg^JD>EwQNA=H481jb_d+SzV9 zWHM{cMO|tb%^F}jxTI-{Iq)l6v*=4sj(yslF_3|f8Habhk~XhvOC@WSf3hy!y|e1F zKV3QXo^;H{&$Hh&km;IE9+A&@;e#|HQm8J!Szfk~l55QusJ4gGRr;QIQOwTW)oh)$ zDn%9kdR-Mk((?3-j9x&jO=e^0xfRaPw<%mLZdGgX$3yNFj{=S0mk}cj@sK}m+bQ(~ z-e!_1(5;OfOaU&vt<>k`a_{*nH!eZfXh$PbCH!`}MUg~CSvi8Pf;mhxY9*6Sts&r@ z`AiIw8Sxa4l7^TU{%C6V6ScKx7R6mNf4n)z2OioAUj2*2`~69i(%k}zA6suy*?}*8(ag!-+-sR)!K`CTk zz1E``x=}{i5X>6;t*L<}k2{auo5oxya<&21M=`|E7|eZJ+Ex#2dTA|O@8A0{*q$~p z=77)*IN`QLInge_)VD;->=~+^4OSf`HNC=VF>V((Mqf^gLQIy9cIMJBd-`+Nf`{z~ zcx5VL#>y;>eOFamG$A+BbH3|3mR`IrDvB`GC^Gz%Zsa(qDZUl&PD(6@5sy9iu1v1y zHC!{wwY_My%9_eM0ebEaXCZZ>k0JBHc;_Ah;pn4(XMEHJZO2PRBML>OHB8GVkt`+x!16n0V;m72Iqmn@p z@Dt=>c6p)HUcgP)+qwaBg%QqT>;&eR7gvMXtuN2f{oP~fPk)fb%*GJ@JC|Jaz^{q+ zl=Ps#^D9|b?yL&rnjVuIsIn$YwZII~(dT(K_~^Me=ba23`&yW#i6F{Wi?uXRrfd;6 znYdfYQA!!Rb*{HjX(tYD5+ISbosYK!`r)*m19+X*u@0={6p^}@dsSW~x=97Jajr;k zKQ3UNR4(asM;i1M2%@$OFn4m*8qoDgaCwP$T4I7u7O9M)l`U}PVsK}6Y)*WgwO5_d zgU`9G+E2->YdR;qIvwd$p4I$;$s3eQ(>oQt?GijzzV_^?O6gZpkvFY57crI8*7a7N z->NifAUAZC-M>I+Xf-(M-sP}y&gF-)FhfQ62BZhlo^v8rN1|2+fv14WZ&fG(D=px) zj6Wcb!tvI*X$ptn8W|bU@Q{k`QfgG#Jf7Yd3GKzH5fPGVrkWrDU%vClLmZesS8(Ho z9`u}e@U;dOspjYj5-S<8Kkned^g%)G`Fc>iCzl^Jun-hSPY{9s`l2IMGtTivX73^K z&S9NxGSQ*uw-+t$WT#7Vy^NY4IW;o-B%Swn$_4j)@JsABL@*q~x7ZqwpdoS5FBVZq z;*@N55v+kt7rzCUQsUY&U=8g7-er0Yu;8zz{h57ZrMb;xu2uRc3-oQMpq-7dvLv!>+X9QF@>LeqN z>3pX@>nkGa`xkN(9FAMI$=4eo2%_L*!0>a|yS zSo804n-2v8hHr(t$F5w)n*sYkHosW2-Mgf-Pn1cf5i<&-f2`7j4E*VsIP@qYWb>@f zbE@*CT*CzeUu@Fb7zEr;xdRINf3&@KSX0}&JuF>7K}EWNWh)8-!5|0-*im{l(h)Q? zY0`TWQ3;>|0#ZURp#-IO5D5yQ6A&o@BfS}Ep#*+29(VJ)&*wS!e*a}=Jx^BFTywtj zEn|$=a(@+XJ%aD~qrm#psaNOhc0paQA7!F0dc@)3ds!*cQ^MWbt!2SOh6`D@e%MFB zjct6AeZ!(#JyFxc6`t6OtrX31^bB3~BrjL?ZoXbkpIlfe^JM z>CK&d?JSm>?HR9h!88|B1BNo1GLUDDWO+6_8?i+|OZYWZ*S8+o6jSwSRJn#WT#+M> zMXW{V{-%RAJ|nmCR!933e5{{&sv%_iv)}fJm#^Am>q`deNd~&FD(24UTKRshnK(<+ zkB4ArKayDu=B_Yd@Sd%S6YPZ>I84&!KRIzVV8Sp?s7QKjS}h4(P^}wtYDrw{ zyvk{;q+Dx|erd1=J9yD&Z)TPSN~&liXTq5w7d2gUgVSK3Ms_(2Rl8|lJMF6d*k@&c80*2d(U<5%lyX{J}Cj8jcz_!W1HsG^QpoH zH(T$ubl`0aOd_^7xmTFF``|~{$Lek!{$3kBquai-Nm6K=HFGIEFI`cKdI5b>fOIITbbZe6c}w>K zY|I@$k(*s>r2E`YMJ_9MIzc7_btLD>3W(CP2JPZfh3cKpvX9$WxCAb~DntTv16FXV z0-1PZsXD2%A6+YQ>Xc;j%B<93qm1yd3HUDAvZjWOWkh*D}@B z|5CzA$!+JeMQ@FPy3cixL~vr*n_{vvCUl=DQ)l})wB`V@< zoI%kcKR?^JHbsyYcnNX8fB%e;cUOk<^zj&N8r{!#UJAvYu~<;d zI&H+SZ^c3JsreQ0IeB?1ZIS&cRH*XS!z<0ZLI8YhK)<8mh(}QErrj$&C(F*mO?hln)uC^<@Ii_3V*<2>a1BsKlkG~>S)&-VidPNK*ihL%$CC26;p$Qz7Lvz1TUc;2?=C`f9-$5*uHn}hv`?K>RPEha zU8_5@M!W4)rT-h(+55#`<19CF#o`p?vok+->drRj!rDY6}$kBf&e*CR0GNIkjK(aS6P>jD{nkZtH*7QJZpP#;UR6kUwi4?jx zHM`^Kgfvv)tsPiUn5ax^9Fc+WcVu4v;OVQ!>GR5bwzD=euCb4OkeP{DMKgM6C?C!b zs-HS#m5HiJTI##F@}%@Ss3|MTdy{ta_>7-hlB*-H@-De*8{Jaely(22 zVck>7XZxYBz51eV-MWtb#@1QF360hl`IgZ#W%t(IGOfKcQ6zcGQB& zAxs39_;XBn#f$JX4g$uYy@PKR_3GNj}by+88E`)MAvK8afWCtZb2=b=hlv zQMeG_vLJXIm-hUVnV;rDODPO&B$?0oxwTV|#~<$H4La4twm{_@ z`ALHcIW{p1@Jrhre&qa|J&RSD!URLdCfz99(Lo(=1&^=kah?6TcluE8$yPnMH~t%Y zhGzQF_8ip9>Fy|0Y8n6Kw!Q}KHG3O%YE28|b#z?ff73R)ww7^usq#Q=(v^O;FI6}jJ)ii3K&Osjg)FCD z>MO&QF436C3E7!18z^1j>>B+;UF_E04eor;{nneLux4)w>RYWk`@fcqZ{F)p=T?pL zj_@o^vxdH}{#g1FwI`jE&)L!h*5hII(P@Hpk9Wr?)Rp2+{V>UB$k{I#_pU2;Yws|0ucHL&-CtO|#++E7zifZsV2;2~F+ zSJst=mQ15cHLp;jz!rS z8=4x=)gy9#`NG2t_8Yc^p6wW7TOzens+{%QzQhcP zOS*;q!Il7jT+p7ht-7dn(U_Ka*9#8@W2MNF1Ko=?xWc6suJU9@f^0PS~RzJ<-_^v^-E<0g|oT zIByFRh;hgw(rc9+dQoAzkEo-eF={X9kmZsqDWO5u^iLX6SGKv1!(F;Kb}^rj9f!r2 zDJXn#oooKTsXnpV*((&%?L~HX;FkuT5QRR^F~1#LjvLBm3EOBg^)vZad$LjtDfDt) zydmecENOoOcU3DqXP@`Q1}c4&P{OPdu{oS?schgT-4w&Jz?}*2y6p{RT}^pK_RG=> zbt46~3^P4h0EOZ7$t6;a)4przYwk@nB!w6h!m_crGC!4ihT>(~S#KM`lKN9T0F~&N>-EmS(kYoT!PneN+AXC2oYb3 z38`~4mScn)OAGkbnR5I%kY}_rt`soAn{puSjpga!KOfl1M-Rw^(Hr+fzjTu~Y8nR6 zsFGP+s(hM^R_j9Cgz!~s#`E%aS1;rgEFcrWyFLq z=gtb9^}MgI?$3E}P73{KZ~x6N9(JjEMks7`wW8Q5gqQfno1fQg-dbz7*-Xks9cX0_`Kzn83O! zKsv9{?D`v(>YwOUBfDZXSo-|<@#9QZ@-^Mu!pbB?buIe?`S|1j#x?=1+obE!BR;7v zgQrgAOxR}294gp!@a$p80a@ zJtqQfkR>|oK>p2oq?%5Sj5S|w+UgLp?l$a^nZYSbj(Ho!Hn$8u3LAa6kMeHK7&qBW zG5ng3Eke%p*=-IUE~&>*y!i^2hZ$YQM!s7hQf!75lT#LE{&5ja9SpQ|(bn ziDt{v+=za@W!TaxVAM3^{DCAmIbUkT(ydi4Y1nH`%=fa_<%2wl`92uAX{g&gSUs*Mp9j7``&&y zYFE|@icf>%>0a~xabCuQIJVe<9|l$P{_$15gI&C_17oRGw*GNH9~9ZrG7zM^D6 zE25vUMEeD07zu4eT!qLVIV(blGJ1Gz5OA-%WX89BfD;OeN7@98D&S9)pbn7L*630c z>pnT_)__sd>H=5%8R2-A+>Y~2 zRpsT6Vnzj?@c;%x(62EVh!z}&5GL!6qWPOL5#cc}WRA)MhU*{Hh4$~%g;HB%_6GO% zV!6)Mca1w4z!U36l;P7^6+HWC=%&sjP*M$(d+AV_cTG9kxoamI5MuETE%m1p zpw%4!-YI``rXJhEJ~CO4kP;AfF@=vaEjpQE;v=GEAVv3s`zyff4iVz=>&!6S0@OjP z37RDjZnG3X>|@pj{RkY*(P_+LTxvFbd8upi?jfbNt@+e=l(jb8h6eX6 zVNAq&n+7VqqU)i+VG*={Uf6=iIef`JKb%6GD1G<8YXXVlaYlVC|@&@C&I=K{hn@?HmWhoc=c^ zX;Y5i8++`jq#kM~)CFoJL!=}3cTI?a0O?=lzvAbo*B4iw&~$!RUT>6-$8z4-SnDxO(9>n`M<&&ROMh0?NCgGaMXMBLl{aUm1E5T5I&4r&& zOvL5NsX!viQr%krp|OF3%Vo%jZdI2ayCVpdzh3w?0^C+ySgrlZv$MVL#3UeGi_FD? z>yMuheRE$?n5tZq*ZVtz_kZeX#3JdX{!zi@`*Rd&|3y|I4RGM?C$}mV_ZB3EB%7TN zBx32QKGz30P;l%CQ16Z)ZWGC|r~til7NjHwXzjf>r)Y{C+P}!N+EuUL%AtV@!`zWH z&=K#v-hjAh;p&b6?%$1r@g&GC99LADUTTJcg+z5cdnYlmQ{nEQqbbC=j@orYZ1T&Q zx0h6}T{{8tQzhjG)^&pDPw5;JfA-`___bPYH$a3mr7%~k1*m`V)$5sp2KKMc=#y{o z+v>kI(nTonLm~=L3AyO|uEuiP1i%9!rfBJZQDU*(aAmMbd;WYz^M<21J16HrDj)_M z+`Dqjq27aHC#suW*LJ~OWoNv-OecOAs4efZ`v;cmU-_@D#lY<2t1_O}dO4*rVK`Bl zD+g=smsH1L+vgx$Xalc%p9X8M4fr1A>fqS>Kv(nIB0v2fJn5HhGyS!7*A|14cqL`! zXm`9J@KO2=#v^C|8Li1fJEm*K!(c^paBqWqspa84EazqcHYstX|8ZXfmJya{%Z5$( zVUW|;(00lq(S{S7u+*3Hq5=E!LBR`9CXUZ8Ouo=Iym?|#S3RruL9J?YP_tXsTv8bM z>;B)#{q5%{GAs&ozr@o)C_n)KREZC~w5LG|+NKz(l7dN`%XRs1LMi#BcFfW@j34F@ zFbW%CcS^9CTU#}vD-Z2PFW?*~+w0t~ZZE*i)NLYB>xz$L9r zabW9DS0=Q@wMdT3`$)ZYt=Vj{1rw|nq7Kv(PE&+IGDPy!1G#@zB;Sl2{h8bNW}7zl zmEysp`>>Zz-*!s{*m}5e)y?56GS%qVzP3R%k4!V~Y3}^m*IydCSB5G_%&JDJp@Brk z94l`5H8roPH+A3vP0v(jQy3Yy__coV^%hl`@XvCBQk!3a3%w&#KfBd4-L<&Zu)1#4gnLq?uj&nRhz4`WRhQIo3?;1!gZSf48355YRPzX<4=sTvuSxWf zKg|ou4E-}QgM7o`_gLj`i_VW4`{5YV{@w5+FA8P%b0HK8$&h?xU65-*M^<;YmSXWy zF7&p};j<#gz6=+KbMXyX{;M*@>&?^bu!}`!6j(B0NXYbYh&fXxwVvtC0BIGm0B&sv z`#V4w!QS>}9O7zZJhm76hO3bz5i9;P&6cztPzM4+2buHA%z=T#)hD)6$#g3MJp*_^ z>)6b%X916A@$LLU{s-reZ-^(&OlEM^DFw8Dxa>hhhh_9NIWb#h?tHB<>1?jWZhP7W z_ZY-ADcu$j;^CTP9p?fn4Dqx`?zAcoSl!fxo5yOtO z1EJ&Ots}8?vTt^{up}tc&E4>Z@GlDV&}~E@d!Sz7v8`~CAYugwDyn<;zuJw-J^r}2 zcu$SQ*bHFh=DMa834H>KVm#p2D6k}G5feoyMrik9Ya>!imQj)~&FuURp7!7yDm2BY z{v3zC!e$Cc8>9_}vVX@T_|7nx+5mvZ#dwXivwDxsX$in+lM!DS89q=%O@4;`TNSKF z$Q@M<82tg>^gm!`mw#6fx>`WG1GQb#MXVu9+?sU|bw%j9t_pS0%)R(~YXVL~@87*k zxAmxSykP?b6s&(cp{3c?Ri4y**9Lz|*5QZ}< zmrLLrYeAjn-SF8gK}~yF0<<`!!0x;7Os-EV^xRRMXTvuGsJLGe!(h$026vNOK6v+q;tv)QSJs{}v?2 zT3;I#-|gZT`rvXM1Q!djq-{c_M%8wk>gH~YEZk}RA;8%!GK7y_kO1cH98ZkeJG17iD6J<_B)< zkG*>T-QX<9NmZzeq!5C$bRe}C7`5NK@3b2Gi20OEE@A49aV?5?EVD4UQw++_BCv1i z8KlEZy~w|bz8RbB}=s*GM^bZU^yM@?#G{aAPMLus;s0 z`G*dj>1%*DU@toHS@U3jKCrc{B-e9Dcq=LRDQ`a^GJwkT1OF#rT^VukQsOPy{)MbK zR_vP1XgQENR21seiT?IDmB0Ep003e+de$h%W77FpuMWZl1$J;bO#IIT*Fam}X%P#D!c|rtx zWWM=7cYxyc{*r2b0Oal5gt$zmz`nW1X)TppF%4qu7~f<6we%IYJ^XeMkq%{69W+>u z-8nDY2K~feaTFr0QYEx-k&Xpa-&fS_vK{_G$cV@g2rC&$K}2V3n>bKfUWZ+vU%lNDs?%}UD}qh0h` z=R8L}*0xy~FA%Fca=FZ47qpBI?d@lJ(}Q)`jtV>EdcJfU{I7k*EHXEnd|7! z7&egrhwliN057TqA7<)3E_a1lL7sB%MM(czMkHPjEVGYd6B^d~C?*Ut2XIaf+Ajft zvZZaK3w(#Zd<2v1^#`l=C-?@mpTm#j#Gla?hupw@faqwk(_O0Q=;$t@7RV!{sKP=O zo7_XNd;3oZ62}r6JkISK0XGc!nYie`(0lE$`mmp&^p^A>gx|$W$M1nO?6`@yiWS&K z$w5R5Npc?y&ug-BN;Vvno@Zh8YCGcz&>G^_yYG(Pxvm z(|uDuM15L0&bE8&#d%4h?T-9^>F(j3M3^x(|EWmNxbfb20sJ8{n zdajfZy~v6cT1*)f@sAq}ht(=Ta;rn^%67r+WbM@bM&S_=cBRyCyGmPi6Vsm$_`FQm z=?H-1uKJjJ7=^C%sPC-)l@h=*WWru6Lvqst_|LPz`D2aW zi#Fsvr}zAFF4n`ea<5Tf{N59lL$t9K_saJU2gLn!;EovH8{ZpWxyNSQuO+1+7MA^lDA+Pp&V-RIDVfIy zRvLFcdCuE+)+pJmT3d~wI39N<{_D~jeL@4pWBVq9Fm_N=-o70$u(NBF;>sv zlZ~iiRfLROo}1!fDWm}S=Jp5grY+j1`~hH&{vMRRpl)dU=giKxpfn^8IAMT>3os+< zsk5r;*|Tk#go#8-deZabEza$$kP*7buB+cDn_r*)S_%|6j6lMnUtgTMUN+`wSO-(w6hC17q5)>(k&E1%0 zD}MeU8as*`AvIi@B?c*KR+KWJg8n!_i#Zf^D3|F|L(39M(E^a455mz%kT-ijrsX*s zv2-&3_60CY8Se24Rk{6jGqC!N((i(?>nsC4paQ_cNUQ5e*+Q(4Up?=>*E+V^c|KMx z9F9&rcM*vFP@#==@r~7ixTFrixnMRixgo4Dta7(my-;FLgZqnHhnZN;ErO{_T>G%V zUyrp@OtfdkCj2mX%wOO3^8&$NO9y4I^BL#~Yj&c^((0!(dy6GNb};*154${`@ce57 z>ANVqE(sCC2^K`R&obHEnUbgfMXjmoI68U)>!NV#qWmpIx{9kPv#mJO>dRl_-QGj! z;5?FOd(T_)UdP61H$oo1^9~xZ%4_EOMrIT_b6?EZ2-bz4xiC98MAoo9_ zR?8qAI1d&6UjY%EY!7n@B$n;QC{D611k&iXQ=GobOurb@Pv}B`U>S#m zsUc?_C5u=_EemY!$!|8-;#X}h_!0Rc7onBvJh{82N@pqRu*`Llc`pW{kahjRNu6|u zvSq#ZdRTJiD7k?&bW})I^IEei&`wLuRc%X)XI$yo(b*{X^&Eya zkoNR|;w*;VVh7Zf^!&O!ao;vm`L7yPItBunoxpk!0Lr0}>l?zcj6R#;n&L@oi_|lt z{{56N`46>?e`PuUCQai_4l?)=G5ixO@#k8QqXof^UL5cvSuN{f))+U`{pPG7Vj@WA zSY+`cLFH)d%tp5Lw}tj=0)7aoBH;cdG=rkuLlFCY%iC7ghqW|r3S$PH5vJ!C@pQcn z|7U62cRmL6l(_;%OFA}6Cqn5-XijhW=oRbOeEBtoOKaNYsH^p?K>=KqG_VGnswUg!l3^${|(1@6WT!Z{^Zt2il~?eg7aa!mvvkymMr2wIQo2>UxJ=8MUTVT zqckaNP1r@nn~eBvMiv#Rz=e!2YR0BbPIAcfS={Bh2B|%>Fs66zFh#w;HHhICO)h{^ z?HJP$v^!`CY*EgB0q!N}2p@wl9(c#%{z%=7cnVW2nFGolB!qa};fE#({}XVdq87u90dCO*cHp`dyAEPa_hA_Uq>4@`P`~@X zK^a|tnow`LwR(1ancN&db?Y{FbZ`!rz_NP=%YFnp%IB2=!qj^%X^in3T%nHY*N#82 zBH-IgOn0@Csq0voFq>lMii3cIf=Sgor$P74Fg*G-!(j5ab`AHNXS{VyK^<4v?9a$L zPqf)v6d)Eab51+{SD+&AxU;iSYH5qSpE#%PLVQ=M^Cj$O&eU<($#QeY6^n>TiR%6D z60#=nQ>rsvqr~A;RTgZ=?uVebkOa*plF{s1Pbzor4Zr(Cpm)G^x|gH=%#^c+F4A^^ z8hcN&H+Z5R`@?{5ihAfDlC^(L;|h13&-w#hC``0X@HyDvZb7~}>+o%-GyG#A@l1Hb z{P&JrdIAbjozxFh4yUXcAKa&*p?UiH{{0zgyIl?YTw1HzKv%AXf~tark6s3JvVlxy zStpgA9j##tbUv{Aw}1=OixpCF@3D8DW1&k?UZZQ?^3NyZCSO47?8E(rHd`ysoTF1@ zgg02fD`$XmCj_R^7MY&CGZYlJN3B5<6f}9@!!c(tUbn=zztkSmU&dG~nUwUOe)c3S zKz!u->D^3B=g_+(gA)5cEN}xy{c55BaMXVoJmvW@xDY{0Mi$X=A$KMSzX8;a^0U za-ahj))=5`XCvIMK|)~{CpWxjnOL? z->w3u#l)hk=Iai{kfPUzShrsIv9e$TK$DMC4m5PhyM+DQOg-wpbrL)HlvDgG=FiT<3@pVDHe$h<19H80+N`hpYNozhMBT0D^2P4H9cSk3bem+x*nfJ5m@a@ni z#PF8Th7K*dLhjS7-*9B#ZUH(HF}*`@RebLg_UCt@Tpn=IqSpxG`}aU#ksP`$(1Sd4clZX7nz_<2H7?G|CL-Mu=BO1{cr?$tGbzg;>A|jFuup(fI>d82t z4`Hu|7oI0kEa#tZ?w{Z0Q654W2Mp2i=?Rg=1T47IkcT1Ef!}_^{Xh7PNjhBw4CBrq z#zo*eDAnBw!6<6cAuk4e1;6C5z7b2}z^bHc`Kp-Std&1eO9y4ki4tIojYdc0*f+H4 zoVnXf2fu4$KEn%vMym`s2IviZjNPEesvOsLH8j0hDrsEDD24I++I!$*-Py?JKBuCh zqu*!Mpq>-2W_;@SqF#k$P^|%piJI->^;;VC8sH7yI~jZb4Rz7|x8lp}vD{3V35E4} zhaC6#(7_)D<{UolkbB`UjY&{kfQHa+My$}jclG_I@WW9|mjL>O_RN>FvD~{e&ZTQ% z+$EdP@n{55H#<4`sTw^uf$yAqbmjdLQoG^V+MB~Opr4HBR#H9JV&J>jPkGwA;v4>z zrulO%Vjn&G-hJ%P?x~fxR5c4>daaBsrkyj&St3cW)+n@ z)#jzIC&k317GCx4ihk~sV{P#U$Bc1@jbrXuJf__LUrnQa<^D)s8ExdOEFHBDJ7LWc zq;)i!-2F1#1%2sZ2-wB*(vH^-bM)AmBBa80f07>jwiMs|>QdBSvAp7Q%{h}^WOn*+ z^eOpjv+BA$1e-#TFeZ1`M9u37ISE1Hx+tB;?Fo)#d!hD=8OpV1B!iTT zpWbJHYh90i%LqTNf4zbRdMaJ%6K&A}k>7>`;(yb>`ic@R{`ew+q2bg;bet~WLN*1! zKJ6sjY7yg+DmSavV#K&-P>Y`2MHfYxqBSo{ra_&GV*4oX>ddig1^;TK@aK0FlOETa z7bQN@_=Ko*9Bij;BgfZf-qAw3*?K(_I$3rH5=Bp^=QYL^yDcWg)@i0)3ws}va4Fhj z$Xv0=sy~8T=N;jN+o>LRB_$svW!vj?bach`JNzx}?V{+=Y&Y_Tk&=rZK5<&svJvBw zY8G?oG%(m*l$J_=8s~p+us`1@;Hnb|6~+i^-|QQ^FzV}7y+c!#1h(K)o>Jm^NU*qk z@|9(&Xkl0%`}S;>L^7ZnYN^?=@SQ7Em`JmZ1eoEVjlJ2|+Fow#9wl$Cb-glI6h2*` zRprX3WcySJhk|7tQ)F3MR6mh;-s8-Mn3+Q>rQJv?NB-2w*}}W6C&=VIENgj;d%wN7J}Ha51((~w!P?W{6PJ# z_Qt7?H5O9&26Yc#h#2H?eo7--yUg@vjMRvwiY8WRMq*Umoel@E@XK7V>D#K`$?v9L zzitj%r97%c;v-+4wYg}kUxo`M8I;*(%6YAJ0tU2(nwna=U8=knAi8E#+21_B^G18Q zYcB4;c?Zb#HM%Gt2pVkF_w0lUhewsg`;8N2td^GUvUzFbCWTQ`gH!FolWe=^kv_dC zK7uvjBdH$BO4&EVSOnTr^9`hWo}U7slJO*lg>}qtN~YPtzP_|2>s#~aAo_*Qks{bIq~2bM zN{=^pX7D>qBNjR7oP)Ve(QywRSZ^^q2g0j&SZWlO=Ih0@JxtVBs&2pKsw6(_N?(W& zP$-D+&(jN4L&~c%bNXIItNUGrbu#j2-H3!seA;*>@nz;nd?+= zRgYDBf?jGH(Yog|E(YIive{lG_#Cwr)XE1Aow@LK*LG^SD{8v^mg&TggAQstSK*m0 zcYue-xOM<={7(n3+V|Ok4o;I!D;kv`zlb3%1PyAh*?gpb6Cy5u_-e3JOS$~$S7EDn zqhr10j^;rBnw|vJ2?PIV-fLh@dCJFF8UNZvJ6JFarL?uctG*`?o^pDI#$&)cr=Y;+~7wc;t&*_l_M!g1r{`BL!q2kql8HKMDYapIY+ z8s2ySyJ)vfhf+m-Qe9i%oC;p?+nE$7{xa*q?pw7T!;);WwN>Ux^ab5-Dz(Bl+LV=T z-ZHPoOgF8w$|qbDt6f%kcT0O=gL^N(cfKUP1Gz)T*K$5PBE_~PdTw*2=0V%nD-rUm z``S#t+`X=FgB4~X7rT(Sq3km^+g65nez1~s>lJVxkNn}G{(rcSS{Tx%d|$(cmUmPJ zg4hH*Zj4i4yVV7=*;+l{$h8Rju%TX3D+kA_R@;qS;_a8RGwiULq@#42l5v^2^#1*+ z<$+13fkQiz^W#hl<3=d0tv0FQ7QyV*QMQ|IU6{SICpRr6ad($$WN;;#X(yH&XQCy1 zHgby{dp7DdVjLagyV6d4*~AnB?e&ju=rE<-?Rf58+RlVF3 zO+Mi&v|*L&69zdhxuwf1%7un5M-HC@Lh!>r9d}x==hN{yN|HS$$crU5{kP4xVarHG zfo@)=nT(E@ORQV#0=?B9!j$eT}lG(g-6sRq`%O7tWHbo`+{6=zl>ouHpLtCaX+TV3?K-I&eLI+u*6HR0#ay=9J zur;pEsujUsV@3RQb-9dT=a#kMio!?wz1_|| zijzWIzHno1kFoBv_=cc|UE#~N%NxZo(C^~r6x%Ib?=;1*9>LeBmH8MGkMRCY&KTYx znOC~H$!Ozz89w>@vBDxmw;ZI>%JayT$7t*PEd=V8ws#Jj*=)c4g|pJvNCLJATIMM0&GJt$;J*`XrdD^GPLzHkb}+zy95n47{nK1swTCQS zS|Lr)7Lk^hQl(-&Qart#0`+%Q{~Wk>h3qz zeaKqtxjLu1Ag*Tns%XR~|1fx$-fkzo*N5Kw>PD z_wvY9PmL3#u4B#G`~v8WmHwq1^XB$mAVh)Et`a4$2jCeHJ0b)1_)=SjnppfRb3v_m zE^BSDt8Bfm%&&nML))4J2(Fs_qLZ`oD5g%UK3hP{EPk20wmgFi(Hi(5k8C5i#)$0& z8ep0eekG$j=#U)_Egut~olwy^MeDg2B7#jPI()zP^*>whP*^{}#(G>}h5A6$S?=bx_9PdhBz5UWy|=G zVoKRsx58g3{p`rohnn)vQ=O0PD%K^9E#xCcJiU0@lH?-8#}Bf{YF;+{kR=wzEBj2} z=~djMiT4xv+fXk(_`G%Xw(R(V*K~_;LCV!oj&6fRm7ZX$trQ9Sk>@8=tBuf7e#CK$ zEwWDZdg$g#)_@cIzRQOCNx_p3x5*5hTm>i4&R#$aa5~omxfy~W^~B7j zwTe~+_U7dCmYda7%D62kh4DzHfL1_l6Wi#OmlZ-l6J$!lZYZiBZLq}S296}>>x;+p z2$v*K??TVu7));5xY1jx>II~`=s=^mhtFqDObcyT*Hp1^zSESsJNGds^xYvDXGo`R zep;@=1+FvhT+KUluv$N14IhOgDLf_i{YfA1myjn^4GIJB&GbMC?ig$cX0tIZTLz|} zy8c%As{-)G@@*zuSV5}=|CRLyY$e=yjUu#i3n_rjvw>e?b$|v0mT=W$UmBk%riZef zycJ4?&6~b^$p-Sr1-nwwy9ht9AwVI@07EK#LJS?zzid1kuay!|QFF>7r((>SA88_Pmx*sLR4w3e7A*Wj>SZJHDLS-5>-gAJ`S@Yso}H}; zW3A@K7cQr0HtM$5%Zl8l@1-b#_D*A2@+jKPdN!obM%_gI~%osMVhtfoB zYDUW1$@FWwtVd%iQgSBOgh|(`mIu}>{RZbKzcuAE;@~d)8^>Miy5IZyHQt`>~f+o_?W)A2=C|*$q$ej9va*AugY12B=h= z-3xnBYRA*KyKEap9SRqhCjGd-%5SBx#}5grRt8lf{irxC2GaVcaXAGo zoeH)at@pxrZlPmx7bPq^OYX%^YU@{&BTz@tDl_Yb(N+U%p;i-EH%nRG9x`qm=jpMd z%+Tp#xZi}Ez|71{s_XGo^%3jPj%!;)wQSGeaxFLFcQSwq8Qmpq_PNDsg5bUFx~Tpp zWQMwYV*aGgUeD#Jt7+H5)6}Y29XFP_jgcJ$@!TapT|K1BvgA%f;F*zJ7Z<}otcb;)`5sq!$lqZ!bUKhzk-+C3bO`60jT3%cp zku6$x@8+RsGaxbwAMz>;0y>TF$42{`zn0 zfRePkVQNdXu%ZMeg3l>(Ay<-Xq1-0b?>edj=bGfV;8Js{(0lzrO+a#Z3J~uS-+5Jk z0i~58L?)`&WeAiXl`Hr_S=DnNJ#cK;YAyLy575A6zIq0zvYZmwtOinAuEwjhQewCW zTRTXos+5>lKwQjBv!qmPQ)YZjO+8k5#XO!DYu3{iW=wNpEnSpGpDxpLr8Z)vrel(@ zg2}6&R2zN(Lx4C+CPUFs;YmSV5_8g*kMpf)d6Sv98fROK+|)8Co{C#&-I&XiRa8># zu}`DJRcpOAO(F)x6D2pt>$%P2#kPYSGtKlacT3e(ZL`&-b)yGx zGF~+D(k0BW7})HpfUku1&XEi7i6e4GXk*T9(km$-1wL=tlNna*G6*-$>gX2)Jw{yR z&#Ks@t0x-0GSfR{?!9Qt*+p_>%eiPRB)7Ar9S{k_ofzjU8@?Q~#8_Tc-d!!@xH8&6 z^AYPuh1AL2LlSUg-(hv%IU314H~K`C62kp3i>=0>_0Gntw!QFI(-SUY4nMW}e>)EB zR;C;roo3GF9Xl>nke0PoT5jeo0!U8pg5`9&SOn4V+w6qf2sbTT{=tPOM6nu(kwE^& zI;Yb#QIP*J7dcm~?&?f=@A^_*vf}nq#3S3@5Foc|zw*Ypd|kG@5xq0XdhcjmLd3aY z^|E2j%1b^D2D$^+bX{P$&(QpCHDE}=%yp@P$nA)NHw*T8;Wa)} z0uj?pl8e={iJ;%k*&Sg@TtHs!-^q=LFe;j0*dPl$!ET~Hbu9N=ST1dSIvzUp%unGR zyVa9$t^$KHQSz#kU%K5E1O3bs9r0~EtHO=>k1n5?q@1Qsg*W@nsIaG*;1`@A&|QH|`|7c&rt}gF(BCgim@VzEt3teVMhV1r*(229=3BQp z5k$U?vstxiRrC|zG6WS;Z1%Nb1$>@z^Vjj$&~@9d3sm?=Zg-2geVNW{w6egkc+Qu^ zkfOSD$5fY>aIn3p&+d7{qP>$QXO@`jl3=Levos;EkN6<$*mA%Whk#_(>m%DrlHC!# zvn1$-GAi-gLLwG4)iT2+*TP;JUiE#@ z_ij?T%g=D1vG~Xi1C_#d>_xOw#ugChoLY~1@7pa>HaoMrSi4E@^QCK#{3}TVPLw8Y ztIAa_Vp2J)Nx3!yBo(LSNzIiYVk#|&>zAW6!s(l2fX=@37^VTje5O9WkfMS(M!EVn zTBbg;T~Hg(xK19dO0ua4J)|fzhB3bZ`}>880eyD&Y4LKt<9)d)S>)CElH&EMJ@;F= zkbM|Mxe(Xn;=cEuz93JVX%GC9uxV(8mNMTmwKtLb)BHBx$5eb$a?-E(>Lr=A zBWcFFGG?;9%<7Yng91(Bdk2q}R)w~W^c%|thL0agBeGq)axYC@r#XMby^)Itx^be$ zQr@XgcA3Zi+!d#2E0qH z`An*KEO$VI);?4C4bfvfL2>)JPSa7r!G%oma_*p`GZ#SD#vFvzZa60Sk<30wsJ@Un zPhgz*$y655+wn!q%EPU^|6En8{4WqFHP zeU?qVk$1Juc^5iNJy}OGOJQk_upxo_A~_>{&&l@ zQR)gyTt4pwYcn0sf^1Y(yy#pHtT_c$KKc1GsY(L~A`RU0<2+SFzTYPZ!a&uPGW=y4 z29_=y-lE-8-6lsyYRtDxtv1wit>O`fPtnubZMb~K;!wLTfpw;2MJZZ{D6?^+##bPA zkJJlFIpp7j_lT)ocGpY`57Nf!J#T%4F@@98D_``FJ6?gX+ynpP0uBQvovp>4-*} zxrtPn^SIZ`UC_GkxUQc)SjiZ$l1EI${~v2_9uH;PzX977Av-B5A^S2}vX?B`cabf! zXDLheAxRiX5@Rj)BQa6^Xq=Q?~mts-#_|%x@KzTn(I8z z?|K}^_b9=c?yJo4rfk!kndh>wJ#g79^LrqE=nA=9uElb6P-oUdi%;}QT*$q>B6 z<8~I}vL0ntO&G==*Vs`gJ9xP*ChzM3q1mIDO~u{Vs_dISO@Tqu8%*fV#OrDUCNctZ zYF$e*%m)-K>q8MO66&OM9sHYhx6$mq!4b<@8Y!7kX(9HOdtzfanoLagl+clL_tr6%I%6~hfy(J`bDNWn}a1b z6=UXFeQv3>y(}~nTm`bqhkLKRvgfXjcROQ3LrDVQp}K96r%`PKElm~X)|X?}Eb&U# zHU&3%E8)tSKPv0Lj@xo-J0lq~o~=d% zCB&>gR+q`j%zQ{0wGo$j8=IyMUQ156Ytif)U@Z7jAy33NccCOVS26{5 z&R=h`Y<<0BQ61D7Ze5V@=(64s9XsGZtY2*J%IDmVzzRfnXC?aY4Ypj;84<9!W+LNz zr7uTEu}EiS^rPr^zgq9jY?EQ~pR()wBj~--UQ_ywcMs9*+F-11**i1%bP&IldCH^h zPTQ}-a<AL0o;lub-+f zsWrPcrwqFQw;7Ah_>+V`SEN_e+?oqsjK(V|E)(1&i~DmCYy5j0%^emz+#Pabbd%Jl z>ASZ}M7mzk`c!>$<25bfOFxL-6~9&cf|(}Yq|=?LH@i}YIna`QW4=F#^Y~lYZFM34|WP&ekX->zcAz-aV1=~!RU;+wHiOQQ4stz zO@1?ub^nIYJK?xL-w?e^vFF8N|E+Ltrw?DE*zPDiuasIE9)Ss=VByO>yCT|@*Ec&A z)R>(S4dVJ&YEx-*H1twhi{HcP z`MKCa5xEDi*v7j+u_i6aSI9Pf@nGGjTMf;jHC(>rma*B1*h$wdnH+eix4WnqC3F?h z)He&}_MPumU~JaAL8sSMdv{>ncA)T#_w1;s?R@b{YT+B*C=i;o;D8AW-SQ6VNnD&e zR8(hVvU^PXwkr?TxNT`tKIa|@Ob!aE9)sCoI_D(kjEu_cy`BpmVWnfcjCW0Gp)>+ujT@K!1FFxOZ<^WT@m)wEYLcy7w;!ihASSzj{|eFN_wZxS#7yUnj13hrFOu zD&~Mt&2l$O@4@`|jNHNF^DpS{(ztu|iDT>nKznGeElXsrJ^`pEPGrO&p>b}=PF#Gu@;L9tc3nbyODNHlM??y}_pD>nWfc27TFNVn0YxDqQd z<^vh)M!e58s>wZSo4Z`QD3hk@2&AtDVUa{IF14Hz8OBSNW;`|&$BQmsk4{iL85JM(UZo>XGPyG zs7GBn*VT+?-8FyTh}#7cq122CFl;^t@5e)_vT8q+G}r&2kXeQU>3Obuo+3^!Kgxht z;b6H~HU{o@w`4AwgKlxt$1E(eSg7QOQ3bziqQbwz9W&@m5UJhvv!FnmXeU)6|@GuA!o9%P2a^Hvv*`n{lT!_IzPp85_{~9RO6j}JvM%; z%)EOZ@1!x;((hfTpk+ks1$*e7$pYbcsqe1#5S(&wt|;?Podvw`&UoHH@HKxP1DJEa zal+9jfU9T}(^;;IEo8h?ZE>JR&E6wWSc&i$wfBx5=mMrin+qsCQ}0mVRcqQvQ|d37 zT(j$bYE<^%6Z`Ja#=MQ|oL|4GUUUm>0-l((Bq*Bv0P3o_EW;yUD4LV$78rZ^?{vS& zCotjPW@Wva`1kh4HBWK7VJW9&!sGRwA|fl6&ly%M58j$MP|R5{R==0?zaLa>f?>;G z*Irqa&?@1}@&1f$Hs@9)aBe^MB4h(lD%nFJ)33*U{3`=&2iS^-C$5j3QXZ4@d6j7P zvBL4l3o?n^u`Co`EC*ocsCh3&P$=;_apPACJs~!894_UsQZ`s&@&E2zYCc&rF_12T zw+vr{)V{5~_sPgRkbC99U@>R9+BNJmcK*2OpIC$|ZM^r& zQikf0eE9a4s}lPz6G=uRD?VM#-mLN$ck`JN6x z&akKbX}`HO9-)I^035twN9L6I^0_p*K)5Nlj?oi1=atk*d{pW{+v_*x)+C{GXq=L{ zvPU1SI4uvNn~|Po9|V)Rn_>;;o|(Q$Rx0oi;KOR9j5T z=U8*=D{meJ0VvzF;2K*TV`#?6+xmQCFG)YfR)%AAHGcJ6sSq+n7c}6Mm?x*I$K;Eq zJXxGzW{8VPZ#u_r4eqP-^RTKewJHKD{j%0y8Q*&?l-^CA*6Jp753XpZxaVQAl&KQC zK4&9yNSl!%+^ewav+@xl(CGDmz&Wlzj@*j*EiHE)h40Z$TIYJBuS2cC;a3FnTQa7B z63l^^fs>s=v{O}5q0Whcd3sqAgh3r{J3Ne!8Sf`nV3=$b)peCSVTP^AUnl)3r^=a@#uS=}Gx z=hm!ut8{iZ$PsURRERj&)0=Yeq+_w)M246N&A&X5;=iA8Y$^4ktMWAK#{LAfmxX!5 zN)5<{e;Sfe2i?}avb~gLD6`iyvb_jfdZvw)-PVGJQ5R<|Q1-e@g9?~2x5gm|zZ3wM zZ#ZL1ZcQZno&hh;kOuW~X2SZ^DbjlZaKaB~>`1Ji1-w5?6lZw0{bi>4pKlmDodfv2 zc3QN|W~IeJw;_}5{JvER)k3l4r5^Zwn89LMG{@*|gEN&z30*QftA0j1o08(**AfOw z6Kq{Q5OuL@Ly!6BeZAuA4!0Z*P2$(8D}{==<#uOyneeUmK3F=XE9% z;aXZ%qn;%0wy76{4Y4^x!!v7uK)3PfL$vI}*i5yAo(NV2opFCj`h-uj{%cVp`P@M@ zeR-EHYlJ@Jt&A)_A>EAMLBUtn#u`K%cMRko@l8$f9e%4x`Em|*cwfX3O|r7^eNe&~ z5kmAxekVzyqQ_?ip*9Xh=&n#+Ve3P4?F`J@W2DUv&szF|k&Sb2?k>MozaY`m(!r4osX})xy!kL&ekMJVs}`=_$GZ@_1sd zOeljb4u13Uo>)DPcY&(iY3n~NP_)sLgxl9r!wFHTJ+0~b=GIgZa~5DijGIsnZ5;d- zYxSLa9`U3aTfH`h&c$yZL2gZA>!~Fu?bjz=`@;XC)EehIQ`_$;)JRVR)jgS&DOS^v z)ereGl_M{6HPc|rHanf{y_cb2n8!xyrcS4%0_a0f&h36iB)=_plyHezciY?L&(yr} zFe9#gH~U+POW$MnnuV>UyYq|AGmBvYAbIIS=RA}-+QIi5$%hK)y*oS_KZLh96v-GB zf4^%~?i+gS{#m2!-C5&%?BA_0a`H$|J-?Z{ltT*6p6XnS^(~2>`PE6`xQ#I!9G->S zw{GZua@#}M**7LIh^``^7mJykoDuQaQMH_QD(kPI?wan5%hS6X_S2@MZzv`0f0CPbCfs(6nxL+DjIH&03a3J7~?(-vOaPM6@ zBT#bj%_X%4)Y-FijW`fe3&w65)Sqv(t8qxWk+F&97ufWHpD36-K;#EfbA5d+c~9u{ zV`BejKdbJ0W&v0L4z}hJKEunIq&5y3$ChdPxC>wFs10~ln)|}+d%zI%+`QJ6!H;(- zB<;V8IR+Rc2=gP>c>GFi-V&io$BoCxtUj{?=%+wO}G1rr=g-3F&*ap_01cUY{n$EzSo;0Q4uJpAkjmZVHU z+GWE0oS<*|3y0QMD;x8fxo2MpRhd0%wd&1uml&$?Vy#6I`Ma;>RQJy2ruv1`71;7Z z%Ppq2z13tQxa+nTeM0$C)09OD%nuJ}(Zm$Y3Zi2I3I^WZyD_tK`_u0@hE2U&r>|LW z5ZHGN@f+^(78sR1iCE>xR!^!TZ@%X3C#-b@0djGpN(*Nx* zjVwHzj4+w2$s5+(G&=p-?_%s5zk1|OSsQ~{cmyWc`$AM~r<{gv8RmM8g(M6^9_>7q z$_CJx+jHS&8t8wj|1yn zy+ma`uJ8?YkRDB&)cTm+I^GUJEO>@QGBA9w<6=>iuxhLAtt9RRFKD}0sG$T65}A>?m)P!PS+E$+8HI; z;h-U?Fay^(F6%~+u4!x$$~&q5VYXlRl04wnf6sW(#PN#cCMwNJL@&mV zuXf{+i3tasTS#x((p1kqIWaNqnoUOt7zS6T?bnB?#V=xRwBGj4H_RK{@2%#j zH2QV!y}Br5lg&G@v&D2NPHUL62Wr~_DpGF9Qx2df!+A|XfV5Apjk@yoB|R#RxER|@ z)8wf1mt3LLDCJ+k@Npgn!IN5q8UGRFjMmsN+)WmRoD7NCyY{VkD7#VQ zaL=?&@7vw4HM?frdz%iIMf4o{rF@KK3p_8}r47^~y#InlM&16{oRGfJ@S_u%3_X16 z5dW%NEmspjJuC9Pjc$bA(u^7S7}*5?0pGrjhikaj>W*_;9!U4AC)xXO8u&vu5&U04 z65ZSEaA3I$&6PTnp{H_$kE{11i#^3Cp=F(G9g!=5c!AdJ`|+?D3y!fI=*UY3v%J1FE*pGqo#tA#G67VEF1`DpB(4hFPcQpg@sRgn zRqjY~eFB5e){n_Fy0F~wSo-K(r8JQ7hNI7zEdbzBc+}I7!<0iAdCk|bGbze=ESi@Ka8mV zB6fwAh`^dw`Mxo}0EGN@Fm9k>Fcsqf4hklxeNWb0|J|)*;j6<8owR63m4&1M3x%<~ zd^vt(vwQ{G|CwOjt;Rbmx%z5fn{@zDpo0j*3UJSeDW9FV+z9c4(zkCf9>n@WrKu1F19M^XUZO9%gp;d>$v;E>yiutD@Bv|;tU+x?! zi~~Wx)MYpur*}0fimd*N63;Xdq4ji|<#%B6sZ^?N1R7SYG3-*IU`^Uin&$e2lF4;X z1*N@rsi_?nr+7V*(hWHWV{^;djcXG{kRSJ~I*MBXg$|n%VBH_Lfv*OC&Blh|mZO#5 zg$G3KQyHmPvCR{aF+kc;=S#us=}1fLqmU4ym$7=;re>US1*O4&{&ILIMt>PlQ$#1C z4T>1WZW+8w*=+b&`VhV6dV`1+!736I@$WlUs3U-|et6A96BolB8O7g#QsSW{B6Rwv z75(G65a5xBAX!G>YV9&ZNjFiowpHVIAuUdnzcjgbhsW2?KCsa7di zhXo&k3I~X(@C{POm((=l^!}h9o;oSR=Sc*Hd)Dcl-a(28GHV9_4Cu!5UST>IrVi2S z{=Q1GZ!=0nzIJbaxpo=!Q?S54*?fH`U3m;(Ozkyka36l{_aX$(AIvz)p!;&STkEJQ z+G|*E@bj+oqpdGkCZsBsAI^CZkR5+geXN^43ww=VWpe)W8kT413MXVjKQ54x<(3(| z31?Ky)?d2C^*#Ffx<=yni6j%n7144g_qhSp@Yi+rDW-}tFFUg8&UpaIkbANnM*+;s`?@E7TK-lzLGCCL&*OpQHh~;;EHl*uhqc>A%ZIw2vvcG<~V~I4}TDlL# zo&z*iXW&1)sK4h*@qA-&zS+Dp%0%7;fNTt$3^0s>MW{q}x#O_TV!z{j{gGd()uOfT zX5?M1eIii*@-wOyri!b|DD_(pfdBR4I#p#=PPvq8AY4kk`Z`J6?=Qp!l`92J3o>k$ z0g5^la1_2ChG9asr@y9(>g(*~)~frCRw98F;M@JmNb*?^1U75mPP(vwsWhuB+7vV4 z=SyV;w7U>OHF8+~+9w9*n#&s_6hlju-zZ5Kl7z+uE)%!#^oe2w;3*WyH3$MQJw6 zZRCJ1zv8)C`|NS5dTjaI+`iniAhBsDv#j}qXSq|N4eC7YpRi&GyfgInZ3k2!oG(M z-I4ZYlxZG^TL`(YVY&%r`k4Ijyjbrex1zqeKV$n9(PF%z%dJU~CSn z$fP%t3-rYL-WrnXaTe6?ne$^C#G=k_kv94Q;fhkudr#n^)h!V2xaPEEQNIQh(oU5xIFqz@hn4qxhzxu}>soq{D3$X>QoJK{V zDMq9xtH8zQu1eS^^K%N-Nj6|UkLAHTlfl<I++3pp| z2x|*4pd*@|yzQ{~NrUG*8IOq_Ac+pjtv=3&be4BV9bRVxqz4gXth>CSY-awl?g(O$ z7Q9sR!`Dd6k=D^<%@RIZ;~f=uC*j5Hw3ex9A@3z!&^X^a_Nae@o5lzj|F*g}nXaDA z1wpmkFPLQ|Pdv!!*EcN*N;IX?B?A*^wAmg$F$|^CUanUq$$2H#kbjGm^RYR5uu_5JOzmUoMuS0}_^AiA!IMyUzDvi8=uO+lz;))K8hv1O8=6 z{r+V^YsDIx7E9p!+89>8?o~mJ9bq=Ez6I5No7-{QZ6rpb&Wftzt*z?70Oar?pWzW< zjeOALYszzRF_&NsEJ5%Fm$p-6sJ64niO2?&x!CkmBB$x+mUAFmrcu3qFVOJ%6vG|~ z>I|Wd`Wk#^ldoa=UZG5d8xJ6hS^A8O*iv*St zfTjHf1!~1Fu&#ahZHB1iPvG;QtD*?fIjbgZw@VMk?_;at436{_1K$Wxhm}73x<8_e z^?sE>CkktQStqfSfVdBs(em)g&@dQAp4}@e z4dJUzVefYXU4KnOJ(03fbe>&UI6}xHvxP<*==$Z^$$4_k@#>isVc z@jx-B=^?!UGKK5yf$wYtW@%qIZN1P*1>hvRZ+`KyA}mO#_$1avMeWdosgVRI!7{dT z<<>e~z0{Wfjk+Ig2xLiYCtgbjPDuOLBSCC>czT^_On;!3VTak@h_O?0AU42bi!kmwB?!?rU=7VFYo~+5NZbK+M32^ z{Q7@BE!&emt<9=;kj<((or{%`EZdpbq9-mvC5SmLV(l_<=%w#;sHXuWB-E%UO=lmG zTHll<3_fjr!{Ssmq3DhGVRFAFWIv@#!2D!I)&)o^MA+6)gP6errlFet1n)5yQgkt1 zLFyS$glKa<4Z{?EhTZNE`nip+p45~AbcLMw9W!MTs|#&ca=%BKjAn!99G=p-T0*ws zb7mgMru!TIJOu&3tC1^Rnjr$S6-fw4tZ_M;MzCUIfwB!e7lX5zT&ROph)I8mdVcB) zwDOEFAhzi%jo&C(+8@|tt?8HEyE+zP0E14CU4QM&+ruVurS_T|z_4<~>cBr>Ccfs0=dho8=yNo_WA15Pt(S{1w zt?b3w=h&c{_}ocjNnpX66Tg)DH;{{a9nkavrxHxarw?O6(V`PL?B3~jjNZ(faS|!~ zdyW^M1Vw&Bi-7^$7p(I;N)R>Z$pp>YOH62uxrwD~jd{31Eh@@+YFOQr855mN zV*#WCW8y9`($mJRIoqZJ`wcDhC;SFR7^c7SkNm^UAt~%Gc6|N+E>`o?TT?Qj<*?~5 ziz=Vs2ayY%?P)bNBNLI9bwz+2xM?sE>2)^m!aZ0;F2j%jGZD)e6p1w^d)9dT5#arX>fSkJ>HB^U zIK#X^pQ|q9KTYWSlbTSnxRL1UGq$v+mNRln@VDz|Sgfq936qm*?0WfvpB>~on}@+E z_|ieugD6?83T!bNs^@59Eo`i%bkbOB@&Ax@@XzEhbRu`2Ji%-0?0hV_lioX&9yWSy zB|dl7==x*Qg7}Z{N0EsxzE0fK^$gDPyr33f6ZO+<{hTTDXOQ)`xkR@T<|A5HASlAwFtDLU91Zf zGsUi)n&ZKe#Mf|6`iwyV7P>xTKU8{EjyPfNUh~c-X}8_(54G2|GA&mp!Dn7Qe~O5t zG7H#-(Vn3~cs+`bZIp=^HS(8>4tV_+b>UG)Qq)M-V25En>h~85JfJKB@+|N5qfUIe z9iS=^0|a7BD$fNoqGLOTWL3t^RvJXyn9$iYPzJ74I6lvH*4let;gn=JM+!yBE82p2qAwaXB z|JHfjP;U6mQa^RI1E?uUivm%4JMZNUz5NaGy}*r69e%qE3eP}csp~5tNg~4-BL$Nu(19?P#UPC1YGpR2gmGOWe8peHf5R>F82W(p1WGJE%=bXTzFUz zxwzicvuPoK*lfC6xC#9JBEo&c+|qX|(AZI-^Ug;z3SvuNJH2}_{({rez~DapX>b`& zXbu4}?;z~~tVow851_%rFwLENJ&=PU{Lb4sq1V=qi3o0i(&hhT$!}R6oD)H=8+p|; zfcn7wC1XJBZb!Uv>T&&2-8Y}AWs>dQiSz~u&47^Dy2d&k?|gk=D#G8zng~Wh*ddJ# zNh431*+6t)80DnlfEapn|v1JmuUnADPt^Bs~qzcxF>0+U@y3L-kg~Q2D1jpEN1j%U5 zrh((s{F{(Cj!A$zi0g``3u%;G6MkH1Jq^{BxZuy1rf^ZW%t=mDp>BRZi@~wBwjdEj z^7}!htibm5W$RZ@$J#{$>_OsVrm(Cb6`}@`Bw?ty?>s*x-vUmiLS>DWB<%FsknpR{ z6CV@~j=;blzzl$nolO>WjNGyYj=1uigv2u&+`JTx3^J#IM`2R(2az>%7z?`BoG#6U z*#jR!(Hv=M?-DL28Cm@VuGrWj$Q##uDOu5^0mV9rj-x|Oo$X93G@prDrB!8ef>G$g zX8p^)F_?LVMPPOXko#8u(Q+F&)-smiX4}{9V-C&(G{VBRh}I5lV$DNw<)q7cn!-e` zmCRvrV#}#DJ#vs?1p$@<0kH9gI~s_4(g&``B{ADTZ{2`Q<{b{rD)xjTs2=%0fuGku zQffdIJxxq8XAg;y(H2KXvvd0ov{VOXxBf&?=SF9-uES z1|%wg=_=dbbS=0AQ0Wt3Zjw|2dDql-f7G5`H=$+C?Q3UAXi#E)0Y)pP>kYB#IJ z&cs#y`F3&-xQ9o7jujcetwm8DPOK4`)eHW4!RH!F?FI_2|9^KhxC4~I#65Z?XT!UdkAHeum z0j$>3jGWioQaGb%$l*1%%M^^?Ad9<=g&6x74GUAp4Q8bKpabB4y|_+YIhd0vVHpUQ z5Ffry8u$B)yGd-IVYIX_Wt`#XW&WJT3`Fu)M9HCCm_x>vWM`=T9rFiR2*LyrNn+1W zKjQyi_>mxq$gjl!so6P`%C!V=QFkQBW`liiPDz$WqHzCH)v8JEIO=>1Z{Mpj*~w1A$L?okoyBmeP0+&QIF@m4#tVK z3(^;iu7bwxf!_^X&3Z?05P$q$h2{@^nk^Ku)7ISF%vJN5%%Lq5q4n=Cx;kkez&h`g zqzrV4_PjGu+`!2d_r7ERqYy-LYxrhdw5*Bf#76!dhj+F6?Db9|cjg$4oIpk2s`V=_ z$yt!H98zILx&I@2_;)=?YavK0gUm*qe{Z%)7##`H69P3mOH1Q&7rU1ic7{;qrMUl4 zd+kp-DNH`+Vh{*dW=}g82-gxzB_TqkzG}@@Ft?_8k^Kbxm-~-uDM4a*MdOb`Pro>A zg*ukxNKJKBRFot0XpVreLmvBJBgIeXc_0kvf`_;N)FmA-Y7sIr!1fRpS7N)Gjv_%m zXr=@6+`@VsFWAYluJt&;@xBqw!gB7^15jY8?v6OdH}flGfL`r6p4-9BgTc0VH_V_5 zJBj)nAcW4fu%#fJ!iE4qz3y-2puGB+M*c?5Vv_(;^g7bdR3O|$BFkI9C`R^>!y}9Q zFn*C!s6S3eP?}#Ng--ph{N31tx!!?DJ7Wg4fx+b`*MVv3(&3u z8L#FYrCkp7N-E!qydljB?qN+}(!A@uit^vOuX|JqJuZ#@2Fm+T92}hE<4e-+qW_+N z_9My5SFd^ZI0XmyO5oE6XEje12mJ_t5-{~774I@%K-g4ana78+_KI<{VdvqZg1({x z_wo0qmQS7PHzBwcrA9Y+gX%uVy{Ic!s6IPezw&uh@zSS5Y8E9q(z&Fve0??z;4pXG-wyVwTyBFv|*ylB|$u zavEC0SyL9{qmqW#!-75MW-zxQ`Ib85j~*F)-Fo7yIGLW2p%e7|J{KpaM=x`P>_y7c zpkAUaFHZ=@;jv8(*z6`v!cs*3{6b_W9L6xV;}!JhJ-HwwaS+c1J5jE0k`t8W*f-K^ z#;Uj~mmIco+&3DRLVCTFmBWk>T>SjoY&*>KWm(MhUjcPn9)j2*DA=G>>mN`l*;CrqMFk(ECQfc*Q7vsAZAn|7uz3rgqg@xd9) zu27-zXV;4J+ot-iQ=#)3&}0O%^_RinBqdD17qd)jBcbWwp?N?)0gK?`))Xtzd@mj# zs$Z@9-sRs9{7lCV;(zlSln{>`FI zhGJ3>#qs_8Ja5w%P>*96%&t! zq!Jq#W$+FzBPFrhz*7NMh3sVdb7VUZ`rKdr(`4KSGU57P` zC@h*>B4jG8k`{&;fr8KV1?WyBUaoR_P8n%rjZc9oP@U z{zvTnOR^%3(X(W~yoUWvkWNESKq8MsWC*7`$msrL#dQZkjUL{xz0}_&p`)L0O<0-= zd)pTU$5`LbZM|j(%jWMr`R@f`OvJF(Lo86j{$mrryvA<ojIP|0Midv^8GO84wxp{#{3fvGW0AOQ#VLuGJIx{O-XXqmB_Y$5G>?{jiEb+xo^ zSXt@KnnHuhrJ@*Cgf07IsL*~5up8=v*ayKu^UII^hX(;R?P?fCNKl$HXRB$atxb7A z8gxR|{aK%%B5!-e_C>3H4W zz>P98V2Q?7DN-Z;-i3rmFQa>&l+%BgAmQNUO*(glL+AGG4{2Mr1bCrJz?B{3fBLzi z*ww>YL7S#Hhl84iUtYsn#+mEkZx|3py08i=w0!dp8jJwA%*VzS^Wx1LZ9BW$pWU-K zZhwJ1`cD3|i2n+5Ka43T>GIFRbmf%H%PI7`RJ7lP5LL#b>v$*2Ta4q}#Mi3=%|S z;zO4-#(S5rVg~Kpn%=Y{UsUi;n8i6GjO7IrK1C=7nJs(q;siffnEm$Qh^`3~S=iAQ zuFcV!hvhXf*Pv8{Up*XAGkT6}Y~=F1nT*Q}i$jIN6G(;9-T+17Zp%wzvLOwghhe6zrt1jC%N0o%xWiEQ%WmtS{SL>h zjdsV_)51{TfXAlb5#i#l1DpCylEYn|z{Z_YXvI|w(s|p2vcb^gDQf?G2g;R7<<=@4 zDO<^MAf&{%iHa<-`DSar&jgBx<-;v}n-7{oi_Bt>_7E6RC}t}Oe5y306erLVe5e?+ zxA#yZoOu}oBeB7J?dhTG*l>xo(G?31NvPr6b=P;^?$LXJfRrc%Yt>SjXiBOjlZ0%zdvoH8ZaoDBBgt> zw)yhcT+}uSrOL&{RoanP7x);CsY()>4@)5{m;K)zK=E}ZyQLcE`H-}hTkmhbe)Z~R zOC%VIVP&;Yv%vJ(-EEgd7yfWXvOu0B2~sFgHs5tK0vxsAhC}E}h81a@4bKH&9|dGu ze)ZUlf-~o2oPT}o4=>(GjZXRK@ZwZ4J4T9?JBXYnjAX5Us`h|fIHesT+UY96Dp(iB zc|?eN_S9Jb4htYic`SxTbDiZ(Bve}U3V}!$;zczY1`rfI_C!ysD3JbgD*kkq{*xPh zrT6anyT1joh;T3$*R5tx8ESNqf>3P%6$4^vfNFFsh z8c)yh`{Pq8up*jSKED@Xn3+R`vy`Xius-rv_wbE{rQ-*rXB4l)jmbqsy5@K8VSMnL z%M2``L*FGc+JECi8aoo2Z06^x*X+qHIA+8hyEwO|S6j|E(A8&WUNE{lYsae%Os`Wm zgPDBqM<{T77w&HOE>K~Yq2kv^{`bp}%8n$a8f~tw(GxV~n`w9VuCiv?s2ZNlW^f*J zCZG%qZ~`8*-?9E^$Hh7YzD(zmIUI0^nM6rLMtLVxD{yVlpAsNW;&&V1>U$l?XAqka zxAG^Dq>ZEqGMkRd`3HnH;`53u6807~{b88Pf=BjOkct)4Q&=x|irR$j4A!LSoYbW8 z2nik~y(RFBVn~7CCJqbBrJj;M;1V4BxUHX5GPj030De1QOvuLcnK&&J%=UK4>;d zT|BD{PX0SdG4S=oD(?gv4X;v=gWcNzm^m(VZ9(mfH4g>#s5uh*-cLLturXHcYdUQs z%u{*3fstNE_+g_V8L7^T%T(wUs!+@ger`d)UWyH=N-CZUOBuL=7Rje6_61F_BCw-$&{ zO|zVa#$BB1&D%0{6S{G z?-LB+pZD$&P4xQI*KRtAQmG!>AhQ{B*{nA3T#tXhfA|T$uUflI*-^v(!~fW2Mp~!H z=PA&w{Wva@Sk-xLj9DUO_}O0M!p$#th4oB$6HQG7>#EVYO*?2^Q+6)o{_0JO8j6?G zXF(mC+Lj{Zwz6_+oANk|$i$?m5W?XBQNqeq$(v%N92Q5{6XC!9-RS8mT}0U6zy#`n zOjl}9iLEZGj>yJ(Ka}o5qI{agmN4+Fo!`b26+td(7}(n6S#>i`*H2y_#?Qr7+%Y{p z(K!m@&mH?!Pkh|7@3CI_X2T3l$G7ZaykN*LsZ&1rL_`c9Ezd7o`d?Ye7^{R^WI2lM1He-H8Ud z9JsmBOGHG!^Bo7@nk?l25Q5uK{TCtl#8n8;;+1^|T}&zN{!#|5sVBMDn!mX~`M#eJ zpIl5AR%B4bY<2&>w!XE9uwa*DVJIxLmQpw+j3{9)OkE=ZKynLrTOtVVESGVzu%D-Y8jfc zD22Ug@{`{5EsKL|T7pOGc-&uK_v+?mpAE%GgPBtWhOFn@OepNfi(bG%vQ)q z4V@zm#n6StQN)XOzU@e=&UN1?cU-$#bx%&*a}~#NusBrTw-#2n?<8?SVoOf-P}pfC zPTb>%YOVKHhR0He?(Jo^%dP&4{k--U{M!4!T_?B~!)A98_T$?b>)(_Mt_MIwa&O`VSDZc1qdox(&U|@s zl81meoLbEC9p@D#G17*HDS{4E4^1M&@uw~lqm2doq2l`?v;ZTdtOrUTT*wfa&^~)( zKlAPNO9R`fg>u8W{Yo&3toiP$fly2_NoT}>M&`Xwd}kAQZrgt_Vb9^<5Y^SQwyrrO zs+C&bjANF+IBX)kC4TO4U)hz&NM@))Z!1P2zc+Ta@I=vh2_4bhGi9G6ZBMQ zK+i$E&s$mROPP8OEGN0mnurpPBbV&rKmd^E6y)H5biO#sGFvZC-=fx-z1~1u zF6uo0KwT`~e%KhD!+b);gZ%Z@9HFXO=y8c%;vJQhZy%!V`SX}tg?E9u=t8P7+4kpG z+$uiQc8D;+d*v`YTAeIUZ{m5{ojTa={-eWtTD(Va8rKriY8}76*-4C!(aV>lR!syrXr^=-*lk?^F$jx*jz!CCor z_k(xZ`I+Q`h{^OUTkSW6qKR9`ZLFh`vaC=HfK-Hf zkFz9tb$(l-oIw41;kGOYJlCJ(R`@l-D|~C`E_BqqU2sfsnc+3#_YQ&ZXL}EGBzmd0dJgNcF2X{F7OSrS; z)m0XT$|QuHlW(*5ZzLu=%JHJW!ElIrc$9gOxTu2TM4jq*!U#RMp zzLmj}yM8H<4%@UbAk{BN+Y%Yl#7BL6x5B=luwCrFUQ)GbuG(Txj<)XcV3h+$%$qlw zZzedRcSz+~y1*omP8Q~LX+NT`Q_bOX{1ivh7N>ThrGtaKO8TdUs$4@BAXDtdF1JAR z_~N~tBY}+(M&jK0j`R0=hE8>qPCdRRmUz7}vzD^P&cIv(N30}5)dyd!r?B1w$Hkfn zzD4AcX%e`EGl3Ej>STJ#f$WT-MdOpMPD#CFV5xq>-_zzZEqKXTdZf&5KplN3=26m7o1j7XO`$YBOneSM zLUyHA#z1Otvs;BP>meH1V>gxTzKEdIc;INP=XkUe8pn+sC^9q4-xkeMAYw`;rB>sr3YA zfCBcEk1)I8LqvYQ>JJ&;qE~xCJ`7U!JP{Xs3M^KE$WYe+cPue9KUqED($T7{x7fkq zrB{A!GH2?!{Xru1BIVtijxXy?9#$%yKc4SgPW6q#VL7Pw zpHjPaRL#_9vExpSyrp(TKEhAkWCnE?VGF&$Xh>NaJR?3*d+$y<;#)Z`{jk zsafHcoDt$)!JcKHbAO?J>*1xO$tDqyC=M%R*5W4qc6U>fO?>&a1@W8Sgm2Y#kbzQ0 zf!W2ypz2@f<>%P~5e~&j4A6I@h-ZDI(hH8%)L<(kRotU)=mLu`RUEZjd)MM69<8GkRO;MJT}u}?lA z$;aD#^_n%2%}N=2)*sHbxz<-OZ1{S(SlDlM(m3iXLbCwg{zSakMV(tKLz;g@^#gKL zS90{N<`PRj`0V>p^l)940}zm>kuOPfG1`=Zple9SsN|a7b+f|9qH2!0-yR$n%F;%0 z@{BkO$AFSTBsm|`^)TtAZNV>bn0UR1GS{mvxi5SR6@N~$@2Kv~O|=8Lg?G$6bSd{o zyYS(`6X)g`jhsIE|A)2r4r_ATwucpvt~BXQsnW$DL|RaqNE2xS0s=xPN|oN5ND~DS z1EB~?FG`W#n+ODurXUcAbm_h3TXCQ5-uv8r_Rn*_`v*LaMw2)1yVhKDjxpw#XTIa` za9^t`qXxv^q2h>Mym7U+cSQ)HCGDLd)stNWWZUKe5bqkb3TD*L7hZ@YDVa)8jJML}PW0yqB(Eb}jm<&C$)csFMQ_qUK z=f|)$8EoPF2Mhhx>`Py&m35;MX8rdzn%~o4h|WkWbB$pQq^q_oJffDo7I>&XPb*~S zOJ8*#ca6*SpJb8aMY-I++^9)2xxIcxB3)S zA^l@@Qb2yXiKvQq;6{J=s4pqps;-&sE;r#esO8-qtlDDS9H0@&eQA$;c$qfgtP#5@ zckaX;_ob}?1)c6IUYptbA^@V-cx<~_EZMsuB5eSlX3c}JC~$*vKKpi59Wh*KF`S77 zsn}}1sR)G_Y7U^$g8XzCk*1`z2oAiE5@N14(*OvDR*`@hP6cw46Yw^H2x3YU$5ZQi z$0PZKn;S23_@5Xjk=h7TV-_EpgW}M$SvbwEDEWGKYrigdEd`k_As-X*?Odu4;bO8 zK^-m(WxGf6W_~PtU|`_)J%0FUa?7q8N5|1jl0(*bZA;=|it&O?b`FP)G0&Q2(sBCr zC)gN6k%S4%C)FZKl8q_8g7k{0d=8PgfzDP;1Q@!Y27%XIorq}Jlymj0G?BeO=g1_K zi|&NRuoqEclW*EBRJTI66xOTr^Xt8~ZX_Nr)Z1*#MAI=DG#)=0^;eiZ!^S_wRv(;m%@n`!o=~mu@%5`bbnuu(4H*%-);CF9{jX|UH{-8Ii4fn2 z(xe54;t@b}F7rHPFm2{o=nMAu3_(fe(HGo)7vhI)pHQluEs&VWZ7;4{o$42}+cwf# zc}72dttS6F2YaaG+M_&B27GANd`v2mJfGyC*XMA&c$~e{62~AS^*qM;$fwU|3pz-{ z`=o!-p(dUB+q6~FbV7{QTE`9PurusB6RLXX`Qh?QknsW_-99znG&Q$($Pd60m6}A=zBNG{5;N;OUs%&&TNRG>2N#xF5l0N9_wAH@5 zw_Y?r8016#pz}q6!eAzRE8i; zZ&A&yvoNZ~3NX*_eoMc+Ws&>1lSp*U!?V7XyDsl|WTMwMKQm3Ezq2xt6}xv3p4I=6;P6ViGxq*FxE9yY(wegp zND_aCU;pRL%dDoY`F%~QxsyTr{|?_RG9pj?novdIko7411%rqHj;wSBe4h?rbgoey zp+jdgBTp>@2j2FTJ`1cD*Mho8?JiS?a+crrm-fvM8UE2;F*P389=`&*oMxKFU}ovn z&WZ4l^^I1H#?GgLa%H{jgq^k|hI%=jMbEA$r}aO*KP=c>?-a@9pPuZpubX#UdpIPj^VXMHJ$XOK zV`}c>s?oq+E4}dz{m2&gO3$_YY}tBXHv9NMn9-^7x<r!F|&lMy3RB9yo!7G?&ZQR5En79?{WsPO18-*R^Ha=gd<+i<=}YycpV5iHaYgaJ zxQ@D#cavR=63GQ30R^g0@v8oowYEX+bc(k_wAW}d$J#-JK-;HhQ=DRJ`|q??!bL>uVwY>YFD;Ff zalT$rkr?M-tCZA|Hh=$o89QB@%cQY-T0bU9j#HRTg?oys6vG>(#qPR15YGmZZ=JGT zcI-E|cOOhBzlNX|#^cxyC$QSXfCgh0zkj_*Ely=CtIz*vnf`pzx#DYB?D3wN?)lk0 z`jqs7bdH|wpa+|^*`=ufJ@JEX2XAG6etOc$JQDDp4<;{7SwSJ!f(y=}yXb^D?H@rn z#Xghbq3%y!nz8bLC@FRYHW+keM^#dxqW)IbQ_j4o7L`DYAya&__&ScOty5XAYIl!z zhayuix%ey~pWBbME0%_;0Zx>mFu3k2rq1QixiCFh6i#EGuEjxeN7nUW#C0aq_dY7S zLJK~d*?HFAKig&d)p~(Bp_;i_sgEY0;E0aZgmhDdFKEgfG5GG!hfS8ZAQ&c0x!-pB zo~-0y^EfRzdxuGczdm)mD*AeaKLQkxH)WzPuX@(k;SXvXxU|sdruro29NgWmMSD;Swv(|SJzKrb5i zq}Ly9v76UiVuCs4XIHp%*@}$HtgqH*%O7ai)~Kgn>RA{x72p9=hINwoixV~Si`t^| z_@!ID{L{bcYdZ;O)rn$PNZ^Moe{kJ?sCzA||3O4Gno|3flpnh&Q;L40d%knKIA}XC zog;q7WYO8r)uhHKHD-711ddg2E%U~l^xAnQQow*uRP5!E7UY3WG+nXMk7AdXa$inM z$duL0Q;6SM9gddx$`R3YCnxu1XT+LaodK<1q-6bEld1w$0?B}&P;!drjOpf=Lrsq5 zYIG3Q!-ASD111@vypNGtuOHP<K`Za5$&_@?tm&S{r?1DxqV&Q3TQ8w_^-@r7BVxrI&x3 zE|wuRY4nC}_R6q(z}$+{*1b`=^y>S(S2|!^Aa7;_Wl7Rdp>akmX6JFmd%tP}?mqpG zFt+f?cqB4^Xx*aX@+vOje6+WsZqU&uIHp&u*t;m8_6XU1K>WP=r zo4r-%@+5{M?;X|Zxv$F=T6}cfZ1A%B5XP38X70lpU^%_cCgT;ShN{ET?l$klgziVPtMWB_d0Jj*7{Xd+kVfu5p$XwOc`|Kng!eT{Auz zogl&}^mjyCQff^LsqI$SsC75d#G|vN9UneWhSw6+T(G9Oq~U7DdKsm-vzbA+)USKT zTb%v%4tsRI?Qt^&?CGIqd*?vQ?mK-GA?miGR~G3fBJS{-)_70ziR@ z_HkB-k~c`a6YO7lKwRf~wRDZKbC?}YduE8CPJI136Tg5!2ED@89w3sGzhV^44_`Ao zyb%y)m(NNbQlRy{;+CI@R`occZR)4RRa~z*3nncZbEbSq1O&g>HDViVH@GdJ1hSW7iiOW%kC({%rc1Gt`j*S&w&iU!+p%D z$YXOp{^2V6WTMX^K|GT6tAd`8*V^8EdMp57rtx?B9z`z(!OmP+{#40)bo8pdB|}$W zKC@#uM>g2uDXmG0zaCF^d7Aq~@*Dm!0)IB>HUN{Jg|TO8uvXuEcikfuGP~4pQ)^-( zaV>Gb@uTfpv8|7ajc9Z#rGaQ&VD?$;P!cxWQ1tjq7FNBovY<7?BI}btkw&znMut-K z{NaXh8o-TJ)e~Po-jxIc|2!imz%U8~7)C)^#wy&VbRvc#COS^Vy%C`zZXS6GWsbS` z$=J~I^fUr?octV6)u==*c zx}(7gKV9jwtYjz)&$xb@{*6tGWR6dH?xB7vQK9#qU+YnDj&7D9?J#O9RBWShPcMpx z7N&IoFe$??t>R_1sWen84mogqr9?!7`$6Y*G|4m=>AH0M1pm z>Yf5nAut!Fs%0p|9|6QLQ@>SmAGppJtk6TPd(R=NT$eP)T4>Cs{S+663UZ81yq|HU z-eC9H6|NQX=(Jt;xWXCG9wzlSdd+_ca+dX0|4sFBfN4hj75b{N7xi&AU68FwX3CV) zu{4!+$ET%ZDSi~c7?Ar`zS2LG)yw2-AoxiE$$w_BcZ5x!Kpvm}n=5utuSb>pvGq}) zXvp^KE3oxCSFqT<{*@MUH|x+e92njmXRnxXS8U+2m0>;a=EM8y0XYtS(T7T|t5)J_X;5r4)fikJhAytm@r@=vw~e(kmX zQcMd5N+jbk(-~Jk%nCc+*4!HN-`}XV9u?Q>RB|(_B!xNxbW#rR$>A^+!bq@*|Ti zY`7!}W7#z3XcctITeJZoW5V=2oMRh` z7If3g{7M4PeEC@B3^X!1-qc-*r_F(w(NI>UI6M2|x5Qyy{jdIo@eCsLn^~~cz+w53 zg^7U{4}*bkJnuBdGn7RVA?|eD$SodPx*;9ng;Dnt#v-Hz55Ip7Pw`z=@xlgImg$Km zXj0T36tedNF!qLOf)E|aq(~lb456GQyqr?gs#Z0ho`Q<@%LJ#v2g{vhMx0 z&1URAw)H$YUS6bTSIn3H_ypSHW-Djq_Sm(vb$kLMSz!ENI38sBxxmn%1MS)wP;i-e z`zE3xaZgzG#v?X1`+L%UYh8L>-Q_4^L-H$}+WKQzhE-?@0L)Z3s;Kv?+wNC!;Z z`U^)wY>Qt|KBWjOPh{xpXylCp{YYQ3j+&b-H;oj1^Eg?M&p`#C8PWcMVzTSH5btSH zl*YgP;WNc#_wCDNgoN<2bkS{mC`3=aAh6M(z&h&a1S(2b+wQKTo&eJWb_K4s9;)!Q z(QXwS{jWimS@2rObg8%Pg3n#9wlEqJsh*ErneEsuWNslxUbXbz0Lx_ zo)@H@W!8PSxI-s)`la%RWE5|CbkY@MzqMuu@KsmU@n=}d5Ar{9fa2&#v(&Fy@>z(V zj*ionU{K+TuYK-!$x+aGP`}e&9{K(D<;}_vlY*4GBnG^e^PeKz}{Yd|O#|4rrEf_qX=IN~)rxmNes>?wJViF!w==<>saIf_r?3Pj)kc)9 zPuuTHRTx#M$-P+{;Df<7q&J4n=k2+eedP{Z3UZthE*~$Vs>w{By~cac2L&> zMIRG!)HepxiZMLe9u+EG-RRMAeOYMSbL*(X54(~2Wx=UL4{k<;yY&kY*inaw3Qs{N zzj#r-X!030>Fd1wnm6s-U0lXG(4f4q$gy{NF5-9M8JFQ@Q9haC@)xUgGh``ayQ$0UF;A zxmz?Xobc&8$>+r>O!jv+8oM3u$KgN)JP6ti3Xu!yrbKt~T*S8zgWz+0CO?U~j|chY z<^fR%iN1*LA{3Oj4~57pd?v<6-EX+`O%M9>1$KLN6$iCA_Uf-pHb59zdPk?~nO%_c zk!Bq@tFHkD4w^6=1lyUC1{V3>*~8ajuiY;)lMm~^JgoK_CnqxH3J@`^)7c$WPL^_| z9qc^Nj^oMM?BHe3wq37wThU22aoHk>KOrJvZ-QJG^R2TEH1tk*z@za}0n8|Tg-=A0 z9VE23H#)%dPEZ)kR}(O-wW3u|J&wH~TOw{7fVzo5GHP}|^8 ziKmi+(V49bR>JvUr1YxHRbAOVow{#Zc13jefk@1urHs~8MEd|wNTGEH?7!@$D_nj% z0t6*;t-H@SmHR643NzS2`c0oDE#LVfOt#w%VCC~wc12m@K|A`Yb!qVe`i~WMF9AOg zMiu%cMf0O!bCxfuQAGbIXjrjg22Y3B{aX$T(iPE6nRmBL%AS~5bGOCu?;FQ4n6gIS zhU*tCl({+Y)y%fC0+u2Ab4fWs8?1qJE@ca--v~SIE%~M6fk{_52OcwiB;tF4Nf!II|kpJz+fvd>@ex0T5RX_005$YpFg^v<>PM`T2Tn zf8iaF^ea6GDw>`y(_Y*GqpkE8hl`nfcU5^(IwQ7<4ZQ(rp({4CyGE+ZSa<7e;xkR8 zud+^;Y4N7&yBX6houO~v9*Ip&soK2WEi9%~2fItLeEonc+;y2X_mW?xhgX3h4-KFe zE!UGHx)V%=UrUVpD9R{ahSDJ(!`43uSXW$Vi%U-C9GYGvQR3BMaW0w~ePCpbLLxYx zv<_(-1dn2%;y7i+|Nq)%mUX@|N9^7;&5w)8d5=zWwXJ~0n#ep!q$}&E7hciduiJd# z;}_P6+Cl42x|-LJ8|VpuvGoEMjA|qv459zwzEteN z{&RGUt;~yfYo+jy%g!6<@T_C41K$Vh)^oiZd<6!QmwP^>5l)E~6?14ht?g_sSFiMr z`{J;Q_MImcyO*AQPZ5SrXXlEAQLv_K&iG!h$F9_8lU@#N@j&=s4_*2P@Q%J2@jh-} zP{QH|Kw2>_MBz8CI6J{OIpY>0ql1FO?y0H~dX9mZNPsYbN5+ZIx? zyE|`v8ZMdLF2RFrZP(Pd^Z2>m3tu>9M%HXNO?Lg2o--+p0_|S7^U@C(F+{ZNrlVC; z*mu|mL-(T>?nejX*0mZ@4iMS;*9Q)~QFXUX8Amd4Nu*_PT=CgS87tk*Qv`$Who0g= zjt)}Qj}Fef=F(!A?)PNxYl~hia9W5gn>Ug1)?P93)txFAY`Db}i>6??Kn>}8R^1w&UVXIjveAbXM1ng{2Occz zvTXE?)(W6AF5>ShqILRUyX|p>H_gJL&$HB;kEDUIe&Xa$vX%cY2G?VpXZJaVrKu4h3@IVV#JgDz@F4 z8JU~jQpY`(x1^~t7%U%{` zg8ccytb{U8+X~?NnVYCF+Jhqt9TAT!Sgl7*2E*4LfIcd5qdXXBPb#?TL`f3}x`{g^ zCa&TX7N$kFm3nGd@2UBsc^q(k0L+CZ+oW)lPg|J|(2YlV3AIg#gom~B%&3~l1=ph0 znagH72VsyMgx44+(o_BO(eY%LZ>Gr(G2Dd4t==E%l4>I@^N6=iLTTT*ypL%XOTvh}2?w6-FzXiy@dyw@ASa zK>=%Ee0@B=jpgLd*T7PXVzs5(lmHNSF)Vv2Bkwyv^MLo&tFrMa-#SYDT3=>k=04w6 z(;2e&b%44=zV<(AK=|j6F_EBtHib$k2TA;taCd8=yVMhpH&4Nj9o<~cLS5oAVZMzB z7}A238{CR5Wg{E#ll|=`Vlea!9AHJO-tM6BZsl{^=teWeQ6#7&=L;AedaJPrS_gOQ z6%LTlR1^1S6{5R%dxg*7!-bnRa|ECpB&Ctf4T#8#zTZj5eL|K#-E^FxzRq492w^v? z7CVcqB>2>BkM2_bEPVPDTsZJyPSRZXTIl)OPS@6S4us&!M4svG)?L1xTIS*ov?*7g z^2ZLFtD}nPrE&U(hLV@BYh=d8n)7z|waMe@$1rQsn_LVUDmZv#)#I7;-l1p5QS9M^ zuz)Agic)Kn!Z6K%J*S{%jfQ>)sez^M#I^dk&3hC z>$*_L7mdx&XhBK(xS}S6fg9*#E!uY^i)SlTiPBV2c@q%<+F6P!XST|THG%Kz2=IMf z)63nRtQeMWAE>|QTv|F%7R32OM|oqv zTE98XlnBR4pGchHam+Lb2%enmIw{tzj#euZdDD|ueYjjl7L4l8UUHI_8T z9*w>&PrO^K-da2yVQNDBZEMy8Vu!z#|CUi9ZCY2X9ic8E-6cw8pt}#i4P_gkoykJS zM9-w;1>1+IGx-J8KHyrgV2{~Mdz%|WC==ic=8FuVLCvYj_?+WMAa%M3d-H2Oga#HH_W%HUh1Vraf;01vi zbMJ`!55DT3Oaw}hf9cMCzn@X?8rZi|_kuD~mw_6SVcw`n3EcvkqL--_zt%k<2*)p4 zP{J?zZhPwT?Bqg=$6`b_Lu$N=VzGHmOi^L;$fvi=-bS41|FVI23YJBerqs_<{2y%~ zUX-}`I@_Q>+dofED8w^5nxr%2j>V{g_Jk?h8@8mnIyySKQ9FExUo{Y7a``K>Ze2RK z4;ryeI}#b@K6xl``XApRqoc+MorRtV z48Z|yej~ruw_kz`rp!|M7&^^f2;=mUtJC0gey8)wvenT zS0FIZX2glJ&ux$Jw^#1~R|cZnQ>+AXa=58mG{SvA7GuWT7mEftX zsK5FVs|51uc!v8jYQK$C`?O*`4aoqB0elpSrsn(V!POHBR>{2iarGwu#no$0$QlW? z55I1uv9z?jWoD+;vNiGvGgbjQBdm@zC{O>Nv?PD~4DJ(|iHXW0{n?POw*jcY<~-IJ z2Rh&UDEZsG!(S4IuZ^!OuKahNCexs+*H2FY&qvRSj+TyA>5e<+Nm=MgSVjF;l!4ej zZq>5OmW7D>?j^ex9-QONI`u&0gZo*fKaL&cr9NT;>SwqmZSY?#X_zT@+iVy?PP0K( zE`vW^+0vI=t;P0!vO=lVTGF&bQDw8wpX*2(8s2#GW}UBWtv)sp{*7Q^seQsIN(S^$ zDc!6%O%McL zR5lzCwtRdlHIdky791*O>YKuK>sv6u#cFhC)tG3XGt}Q3@-kfCmb5^F&VB}3Vy>&i zxa@+mG{sjPxGG8&FapS!eFW^Dr=gbGFk||6`+DNdxIwdV6C@Ske z%~tlLP6T`R?(Kq=>rulI?7_hSvbc8e_VM`-jx*1Zb#9J-^=vrsP8LX?VhuF(cAqN@mNN(4ZWm!azk2F`1PpXvpC(f<_d!3) z=D0Hrym4gbT;ds4)`7>6?`|@fo<#9fw1rO@7nh#@AwKyhTNA4*%(9gtuI%F|(uMg^ zt8k3mQey576a3Y1^NP!cbgd<6G5+%^_9O1ICl}ax6rb z#su=HF#-y$yT$8I7wrp^h+=pT9>fkkMkV(KkL)-UOac3d(8e*G(Soq%<_iV_TX8xX z%s~q21T|{cAmrx}@t@tQ1mPC`E^KAXiI%5y;~5;1bMcK)%U;Jf1Sq-Mj4n8yz$5wL zMp62!!Fy4rm}ZOd%d=abnDZKDG>ugjfe`^ZK^Wf5vz2MzysmQr^iGOTeG;X{7~_T8 zQ%3|8Mz@9peP0JMm4v|kURLDZi7(UV(Os5lvJZ*ivU1yZi9I$>xxsjgapRIO8x!d5DT4rXix>GRqtl?{G5Cw{Jj=7Y*W$!$4 zSNqpw(Eoox4sy+?qFjbl_Dj!&;j`f_yM^FuI8819w<+V+a1?b2F2saC`gz0uAEzdg z!PKiFDsvp`rx1VuH7*E`=kNSx1a|n;?p?|sR$x!cNS5&fLwM8dEJP<8+fSZAO$B7- z@F8vR*xTPa(h*6kt52%Y>_Wpr%b_Q2t#|-X#Q`kIt)-iO6UGS@U{E#@fEGu$M0?( z>L1y2w}lx~_$&{411nbGDb`oRClD0~I`l1c&OIeKkNX~1SH0FRPj=Ddr?`>AQ>M0F zyFfPx^0Jbf5Lq5Fi!9?m16GgZBS=rL*N`aE^U+YrlgTbfX^Ja3Jmr<^fRm&7;VYTe z&FQ3xn~k|L=Y8j$>5vQ}!@KnLWiP%}lN9}o(AX#tEhuo1($>`+pU7$RU*Yt$Lacd+Q%Qq>e_CO z-u;)YK?9!#?y3oXbtlT3*Fe~>N4dA1E|6>B!-3h{Sw*bKd5e{Y_nuh9mC$+3n~7=(AA#*W6@+$R)tC2R;ky&%IW zDoc7+LIhOcNn(Ba=|3WL|F1^qLjO5x+wn`fr?}aR_E8hCBGQ|(F=9CjH+7>XM1TcW zYF-|wid_AM$@KM3ojX=+^&mTSM(*7CAK8!FuVz2LbyV1EMkjLhpVx{%>XfK|6{PEZ zEl7tJ6B7&*8C2)HviM91N~3}*<)aqrn2(n5I#*?OYmtr5EG;(5^!e7LCGrSEi5h}!k2$p)jS zH1ev`DDqe;iwll)oEL)agCW89S{O)?t;8SGM$uj87&)F##vR<+nOxGj$uD|*`WZvL z3@?qi{qW6#zgl9oRe$t2dDCOs%H&nlR*u^kpl(% z7!u>I0F6vY!!AvB`Onx#5j)u5+$^VnZX}d5XEY+1&mJw|qYkH++N>RC2o?Pg0wDgL z!Qg9k8ftl{BON|hvwa{0+z5s$8m~QAWk)|WU7x!kCZzU5y@nzQbJvStO}!FT@_%5CDo5y zO_eSe$ z3mdy^h6xu1qSy|Q!%*~1j)QYz%pyF6I_yX7_FtS6_1}|Em)_qdq`muQS0YDJ2PnpF z0-y=m+1Us-i0sTst~Pyv24-R|X|TgL4gjc(E13UZ9&J`v$bgvxW&G5%;r{WV{Q1Ri zpo9GAwsL|BxZa)BouC#Eiplgv?H9Pq&G~DR`VXaM3PZgPR~v6Q`RJxXG+Jjhy3ju%|$;$O_~{_Jj4xAAVW`>zbcUmAeQLLkQ9GGds)N0my7 z%I+=?44egJ5)^o3o-%#^1V0phuh^$Fp}z+}0MVI|!(hE|s6C9o_@hq0{Ht|3(3fs; z$W;=sXIpBk7#QGS=!;Wc=sGSZf6LyZz{NoKtkf9Ky9{Lu$eaMyui9$;?7MQTNvoO~ z8e}6TPggEH2W}&_PmyZe+^tVWGN_2TaNyfU$scFJKbkpC3qnO@!I&T)$LG4g*@j8) z7SFbWmHQ^F)26XBzH~Z|H6@0*^TZ#HLgtmVC~sULGO!(X%2nLW3!fU394lX~@WYjH z`&Z*yCAe?b1KbZ}prR(XynQ~A|Ea#f!6?@M#Wp(yZrgUmnima*yUbHtuXn_AH0s3T z77tFbbHtRZsbFm#!P!%XFNVS%|wCamD z>1vCb5QevSsM3PtWD=BUFH@a=?HjScamUD8mD-#Qv^aluFOcW(7Evn#wc&>6poBBF zW|*5*MK4-Y#V-GmoOw-WJu8?|#CV}fl^6uYnJmU%GmL3BSS>#&m^#Of*0;#kY!<{dZ90$Eo%4*PgBm^381Kr>7(`d`0esq&K+RddB4r z$j*ckFoKHn8=a;`)tLvXzQT(NHdpv^{ecfx6l3_BFutNY$yHqDA@*yT#|$(Nyq#DZ zaDVDuG)Xz1avuu7E#{}XI@&EF!m+`i-lat&^D5){29Pn48|7f37ICT{ytuJ;# z(#j5#-+bzU+uS`@er^$)|lyvxlITF zlChvbQS5csIDtZz^bb;_9zJuI6-Hk9moR{RqGFZq9~e|+J81t^nf;dl9!a9}R~DGD ze;r&M6C&0-O%76irmrvnksYY@VRC*zWGugWhk@r`>QV1(sa>;QIv172o86-QF=skA znbk`D75qZrWar{1^q7EPlNuzX**iuXY(S>I_lHsNe=Y}E;NVL0WItaMX!U;+_T&)( zVni&S|B3ER5ClKC7HO_~Wti7wMFF~z&*Y(B1oc~SI?cWncYZ%1C<@U}T*erh%^kj!p~n$DNr^xCZ|ADso?IMT#O-cEM>1}%)meLvpVE+IDd zhO(+^PHAaNylfl@s@K2O)q$f#z@FSK;zz3c?AOwRD<}X)6hy3ojE3%P2Or9H=LUgFXg413(`>)g&RMV?1~pF^+WL+&G}iQCfe?DL{Q+9!9>K`88M^y- z5W~%r80x~9O)=Jppm=J`hKIUCxFGNDID`LJV@nXKyRm*}O7YA=$1@$I4^uVb>es+wT0GICc|Ds1R2IpNpIN_&2v~yNFmKeYY z^dNWqnBLTMZBg2BY;gzpZAr|w2VIwwgMjz7qeI{IfV!MeccFpy3pO;aI?nt~%r#F= zcAtkvccaxfeZu6M5%P{qNsWlVvb(^<3DAL;Tx1INJOqv$H*bHC^lC8h{1M-V`}&PC zKcN@Bz^dv>vdaSH^J#9$8|QFGI4|?h@b-_U+3h&0Si8I{is?=!D3(DA>{ky~jl&cJ z1wj`#<>%XiG|Z76Yv)eDrKL|PlfWCrJV4y1&}`IdGPrnJwte;Q+FkzC1=EYgk%ia^ z)BdWxJ*q%}-w;sW0CZ=@n>RX!hSYnnf(3A#<~=QTQy@~^p_L$%Qv8wVT>sTX2h;)r zt+~#z7tFNz&cwC^N*6i5pOB=$e$Cq$GIp`D1nn zYu8m(vj{snbZ*`Ho-M-^D97=`aCGrk*A|Yy1pcUMHX7jf5x96X z|BFSFliPaBu3V50=4buL4HXlm0v;9deZh1v ze{=qCn~Y3WR)M2b6y0AQ{10dJ9}gK`rW2))!X1v4^_tRZ4u~7UMT=KGYYq9WixZF5 zE1Bn4O^!khfGa&}h2dF1KoO2Z7O>YR42I}m+#ex9?nR8oH=?^}qw3u7;B`}O4=#HA zQz+aYqC)OHszL=m(p**@3o9>K_I6%ZTZHbO6A*|bB_j(K*>nC{rAimQH+a)7pSxtJ zKel|>U{;diDML}5Y@3DxZW+T_|MG4Aqvd(K0nVQu3+9D+=4J99ma1##TmyER{WnF~05^4XcjR9jtHcQ}!a(h(xFx6Zi-oTfKpst zYz>K6qq3c<1_$l#?kiXGAG>VjS9ci>vbDeP`is92!{v(n?`j|~xUc{>F|C1ryln?; zVSWSsmBAf0ZwhoqVotJw&NS>fb1j%D2DVQXHQR0 zeILP!>`*^e|yU~*ImZ;xS%>yxqj}h6T`@WzR$d9JoBm4&#?RWB5-xo;&g<6E7M9$Y(ns?I-MbN zaKantWkHF_#kgFDM$)7`nsLlL>4lVUGC-cVEEWbi-D}6VKAwZg*UY8H(Ceh~hVLX2 z#adZeYH69Ra(KJ$SSHTb&ki~zB!|wp0+Q8IXwM;H1%2Di!y1a8V5qn=5RC%!GA{^;lfP4=h~V5 zIRrO9tE>uFm3{>F)Tgl$kmA1rFe5U|e|Z`ZoZWl1=cp-ralD=6{NGr-{5E{T=f20k zDcb*s!gue3Q}DmQ&AQ4LZiN%{PM~n(d`by^A<(46T>oX32Nm_3&p%!IaJy;&4X*hO zFuX;qPH_vg(Juq%zroq!Yd)*9CMgq}bC%Cz$8Wd3qoKvn5*_0Im-8o}0Nd;_tqgmC zGSmd{hTbU#kJa4UQu@FVBp1xU82SWMN2jGF*@6GAGK*`jjOhI4e@G#J8XhbC}ZUQrKASIX9r=1Fx-C6hPDte@~C~QE= zgnK?q;Q6d3fp~Pp!-n;6q=U)Vdj~-3!`oqO$kv|T6*hF2!_;yVk;6Y3V8XluAuyX% zpZqxPwn$6ot=GDHnqW!^x@Y==N%9WcmuFtapR{N}S$EjsJ92zBeC_?SS0{*z>F8`(!c*<~$LMo8rFPyK}!umFh5fC88WlVEBL)L~r%nK?S=`GJpk{3+wD z*|$A!I7Cejc~k2E!;*mb-<%NSaOXgub&CX}#t-fF!cdAW4$KJ_8T<&V{{*z=IsDQz#N-_cMY`@vy@EWcb?hI|a>fdAZp( z^QoI|J4sE5YU!i7)2PF=@5>C1Gs2oMuV4u5LN$!Arm|;Xk!`Zeu4#FP0=^8{s#1pj zn=d(w5%%|KQuZ%ECriC`QP+1t{t;7#CC;9rXp&rDzci-ikH+t5kFpFvBI!zN&%s_8 zaW1R;%*9#O7=;5)5G(MexmbvLDO4EJpM>T`KJ`p(MtEv0L=dAEIPQ(lI$EW~-R1}? zzkLDWN`XX7bH1=f3)0=N8pDTM?LFpZf<}hM8tgP67-c`XXxn@UNqIgjrGH8%o}oTv zTRDdz^%n3(za!teJARcGqe_v`A7mq7)2T02DzEmlG?}^~h$~F^jpsX*ctJvUlB_&P z?lP`WJmC99+5!rIFYNa>MK(+D(R4zvMifIsy?|jOu%JS-rHXOdM){e7)PABx)Fd4@ zbfVt$#IK=(k8r49w5gAB=d68NaP<=f@I(z9TRj**1Nz!}sb+5Rh$_(A13X=}CdJ`% zcQ1jERLIkH24vg@8ypG9!V?;6z{8F7!iu8mW z7{LUUjC^^yPl804@c4>~Y6XM3pT#aL6V>XdeXWheY zt(flIa&L^NmXDNIefEMb?3z>96g3x=3o9d%-0kI-aNo*m=Zxv+vA*)z@7@kGbsMN@ z*rL8VgG0S6pRAEo8~zolHvCft1c7LIxt_pQw7is4-8;t@P$%)2kybF7WOo#pD(FYJ@`WUNGtlu8$S0AVppvU zQcR}=-?d%UgeztISJ~)5g0ST0;MJ*)A_`FC^}H(4ayf8|UxV zQLD=Q{I*>TNZkj3jd;&z#upWQ(WE45R_6J|V~)rfGpZsXz0pB_n6JH91u1P#igI-> zw9V-0Q;8rs$MZk#n$drE*KVFC(tThQ%drmEJLOE-X4Kl6>}*wLoZO(9syfuENt;BU zq4sQkz*2!B^>IBrRU0qIOXu9Qt8#?F-O6Q-e>?z3D_Mdr1M?ia8I1bBUxC280f>d8 zl}J?T@dAI9m#}1=quxm`nb0-e+cgeCXzZ)m!`({@yTX?)F(*qqea)i2_whO6R>-4I z-AQ^L2M>9!T&E;vBS4-JB7)0c^5LNp|8ZfI(6>1lzD;mDat>J@ zt7F%f%Y#;Fqw?S9;U8{Z@%N-#brCeCS&h6XOXUyah|*NJIIVLvIDnimDaLm4w{?fR z{>d&M;+G{Yw}!jg|NPlMzP7%34!jNBWx@bUAI3{K)gM>F|3HypC6?jKIbs4TJPjhc z;JJgOwLDVzjK!aBPL~hzRZCOSe_R7!$S}hiUp2gk*xU^(HW@$l2NO}up_ZgH$`4#D z{h*_f7sp|gxL*|?4e*&y$`H}vGaF!ir2t3&d4I912~q!aYr-=Wf;C$#f{?+FuMO4D z(vBZS7Qs-$$US^rYzmO@iPRlKtAQ1~&XG4z_to$2W4<>bU^lmpkx!aDpznAwu zj?_*rvfV-JGB^70zOS{ zuzvO|>VJNkWcBKd$h{+XFkIt5U8esmE$T3=Kmz`rzO6ajw}`r}y}hRl>akDGIM?V) z*EVtWaKx$leJaf4e3bI`;D!BCB!v4N6N7OLAf2G&zZ#(qPzweKKj%#`XBD(el$l`BZ zX{2XGqTB^jpRr)v1z`$O3G4%7_mo1`p48Jc7pTPX2u*)PM9R@wx!$SpNJ&pGWGF<| z(Pdk8OFkF1;E{AXWLO%lZy)ae;AFx9LWK#QRlry%1pg`8k}3ouauU+8+=MuJeYKbv zwEqM!-gBSqKBwTJMLyX@_LjUj&(VsKqvpaR$C*>N3BQ0w3Suggv+~dlJVDs<$ab06 z^E`!$X;Ro`Vvpb2+LN_+->oxz&rrmqMJ5Ov$~O14c~!$;&C{8-qtq{)BQB2Pg_=!- zJj#5%y6X5bmRle}^hKePV&Akt!rby$^&_DYiBkeP*~HU5`T>O#6&k<=CM{1tqvpz; z?JcU4jO5!?P*w^W)I)-ac2zRCEM6AItO@Z1ec)j;*`bMQ2F@(vG&zbad%m^B)Gc;hu{z_ zxCeK44}rqng1buy4uyLlxCIUF4#9#H1b26MKfa!xp0~QE=gnI0FOpSIb?ertbI#sp z-@Cu^M(5?*PgHM6?ErUy*EvqZdEO|mCPGJuZUj4W&{0x;^(E#H#ADG(B`8jG_V5Ri ziWk(^PB+Xk6v=S-Ud%B8s6pcv1P~lmPtZC7h}8enPlI4?=&xTz;)82ppxD>UAtU|; zZ7`YdL)qnlj1%gez^Fyapp%yvnZG@fmf-kk9*u{BZt9%vdJB^<8u! z;HgoJH>x+C%uHl?1>SX8|Klj;glQ4-6{ucp4F9X@rGvb2ywa|;Uwlvy>PWt9fr@$N4RURz;QXyS)kT&F!n8bWM(F^h=fL6xOJ8>3MX&Ndr8+G|GRXpOW^1E zOs#u!%9jEgbbnnam!fZXy&d|SnzCfH%#}F>!=x1!sk?13jS8O=)akLzhpKKq)#84v zeD(!Hb`b3hNGt`M)_xkHOrS9^m;Qnvv+`oyuEoSp6ZMW2Q{dFDxNgk_sM+T}xj1at zH}2)r7k=%*N5TN!g?XUT*W~enIdS1r=b1*lk!mV;xI$Rl^(Z+hqPh_>)a5z1qC;5P|L^Gu(vBTRs$p#uIV7YKu3Csntw7-C zUFTMo-2~<_3jx-URKCMd41Vg_=!I1d}*jkIB8N_7u$~fU)sabFC1mLgL zCTcdLEM|6&vBdG9VHRpP*N_~H3S$&9`A|0p6H-o^?T_s|Pfl`2d0E51FoNFIlOA1Q zKxpeqOr181wA~toN00a&HW0_(C|XVx6v}y{fzY^)JMP&uN|Zym?0$=o`98#UQ!86i zQPz|md){4i?19nv2a&R0I?8P0=}U5myw;6RGjO>8woNnXTktMYhY~*in0KJUbx<(( zGW|(2R9D*Emt9|fha!GhLwpgQ^Ed3v=on)z#Xt971Lrrzwo;%&6)f#}k2_d zKj~=2LZe-_$4G?6UE7bKGfegBZRBj1+zR&RDklqMOfCDzY`K;k$EuigX8-tG&sAcL zS^A-6K>rEqM;I`!>Q^Q-mx7G2s(a{TKt@bUP-=nNO~4@VE*#K?tA{SCcyQp5km5@6 zoFmYP9liw)fc4Zmj}W?UA-ud+!DVuVwmf@>iU1K2~yr-xf4Wk^@AI%|i=DVEFg zlMkQk-dhrWw_td@20quB!7M@eltM6Hk$8kEPB-QKBmC7Q;6PI1wD}EZuG z$>YngRp)i8kZU{Ajo(vmSl!b#XYzN9X;UYtu}op%ac`NluUChw`EaC93$=0)_ssmm zME`%ItUTm2!*5Ya5AVqNb7naN;&O|Hyy2?tgbV7wncLH?XtkO#=ewDs9|pCph)bI1 zC(y>;yWGmBv8y3*DYKPXH`lT(ms>JgtyWOASeh4*^~dCJTKnb_aM^VR-Z(FDDb?hA zd&%~8T~6At)|wwyjwzbz;3rc@@6B3E|8U+e1kS>hTmoHR5%Gd^^~M9%;>q?ggLzgG zs>zyUHV4Qq)(HO(fjiu#o}lkK!J!G&o77ejb^A5|i9{gbGYo}t-j+Zi-*K3Yh=ay6 zO(vh-#ZH5A0=?T4iWTXw)8v3I2d(#dXU?6K(|Yl%uiiQ1XtuB%Nf^loF*VgmN{Ld3 z_>YO)?m~DcVb%W6#Go|6!jE_TO2lThX?n@dgG9%eKpLC8pBlmx5#ixjJr?Ygu})cZJROxOt<_cJNiRo8jt6ChKkcsG~}%7j55H zCOPd-tp>6m!IMGGFnO%LGvN`$-sAZ_>liUhESLMjk}N{cZv?$>{1+SStsD~?SC#Jd z>#bZp_hTTf&tiNI4rEE6UfpN%yWchuym{Z0;?&WZBb&VfU8*~qK9ox$jKniq{K*|$ zb`V=@ar!#FU)0@NinwY1VjZUfm5598e&vUA21}Ye@9%@fwjiXc5|aCoOzs4O<9_R2 zmC7GZo4G1fG$rhpI|~7M->2pqOs_M5n-NuX-9~@-+&A2H|3ro3^fhSv+80BXmUA9E z+WRL6`ZYVj0aOG;3jq{bJdo?(l?MGb)QAF1m_-@(8VQ8YNng{PP-@FoMM zsO|UQQ^cg%_JgN8rY;Y?Pc5#863lvag&g2cm$O923+{}1ZI<3hJm8wJ``T(b!Z&Pt zdb;&iX_w|<*Ra}u0CPzw2BQkBj9ReDg7S2&1qrm=bdi#=;{!V`i(k13mDN-MEb5J9M(Xl2pE)(?OjF zQcTQ2a}IvXyOBYcorza+3v0&04nNh@%gjk)qvDEi8BMI`6FWo9il8GD67>Za(KP-^>LWfxSkwBrj zT^oTww<=~vMsT0ftw z6-Fj$R6RbTuM}kZ**AjUtY6w0crkX~HD8wP4kZmA^d#uRp6<_7^lwwkW(oXIGx6S5 zT%N5aFeYFvDc%G_4jRs>{J6u<8f}*f;4r>~a(P{WFK$d@=-$4CZ96|ILkl8K#vUMw zdudoE0WMe(4FLg&N&?>y2Xl`|Vk9~+ugJ$6+^#eTy_2p8WR#vM)Q}!bm`++8f;|TA z`nZz>?C-9lE)QU8{MSRfN^x!T-lIJvb)Z9)h6QQK>W6UvsS5Rc8mW8+rq7y9plXre2% zb0gUcc^w>tNd!GpkE%Wcz6%*U1My<&39x%4i|$1O0|yMl^flip$4EQV)a0sLyz$&@ zjp{ro9$PKihx{YYwJP*un%#~>Ly&NR`?Id5Ayn`hyk92kUDBKq1@+K29XfpO@!(Co zlZIwXty8jaFbQRZpLn{ye#Q+-guX*9yt^dZF0Z6j*Li2yn$z=;#GtA2`k_~@iCB0% zf0(#2rI#D#6YD(p?+XE0o@s2K)e2@n<&cpDNMk(nj+g2~I945|@97{4_HeBo?SN~Y zxhEL~cz4wQdJccAwO{_2Dwv~<@Z{;PEA2X!`U>;v<2kfnR>AkEE47&gB1M)$T98f+ zC`Z>|D4cbLo+GY_eS&yHtw37zeEXeR;2HPo6WQZqrAWM$EhW?$rWj zJ+5YnNnL^t-$#3xPC@W|eW7+W*2MztDg%55uXFKqbedY7LL!UNE6>Z#M)kY-<(B87 zxm9l!Te&0#;7Tz*qlj&8yG0Qr-^*&VUow5sguAm^E4l22!=*l%ynTL`z3A!%(vEIT zIE-$MZ_zIHm`N@J`y6OYq<$&?YMFKDSXfMR%pVq7(!HoY3dM~L|$0(LEhWg&6glPr84zbfIr zH)`AVs`|;8Ia=7oq9Zb)n+`7aRJ+D(Sn=@=t>~ME)r2OOYg%vYK^Quj`ch2|1R3TW zRl6h>rF=stJg!M_{W07+A55!6YPx+ZQx^k!GH{g6=laGi-d=7rqrRxBs@w*rc0rT? zUFzo7_eI9Z=NU_E%kh=vJx`2<>!Vq~_h)=l=9Mr9++2l$+6L0)9#_Fs;qcjI>~x=k z-_-5-_6*xu_Vo)!`8f4 zz&t58_|M$YXf=#6wPuh9T!s=Mr;Elq@7s@{7VTfX$Q!1X6JLbDx>rWST&=qc`7Fiy zhq3?)rPRkR)&HJ?cG%*u)=t0N>YBjkZuy-Hmr*yc{pP`I6Yu$=7Mf?kx9bi=QAsaZ z=xK+se$0>{o83${oeeeb?)^ny2Fql{GCy;FvN_wMRCk9m8t+D5TprcIfb&@oTeI&5 zU!hz^?9p-uNCwxMcVLvLz*0Jex;{fN&>8zp&(6-y+QS*m<1b)G!e97o)PM{X_nn)u z+C{0)3gR#|X5)(6}#xHN6O3L`Z%G=B|*OTXa-D){3P1K#v@ z&eJ2DTKzXpi>>lf7<~8rCYv$v4;>p}>4?L_kW*Rp@rs+6H=Jtyq7QN9-?g1i9ewUm z@)$JArbC5)O&|M}7jDqT)|_P5GQHhsMS5`0Hr-`oW79qT-EKab5YctZBJmBQ@g8_-QaW`TVCc}bbOAXMD&l!7fQ%usjHM1m6p#gLpb{s3S0*q+P}*O3 zMS2iIK=SdNu%<85dHSKPZa<|v7(@e#S^@tZ;zZJPd4N!Eo8AGX)uB3Ihomyw`%Vac zZjN7KYdfTDM(DWhnXhHq#;rN7K^42NzpqcZj4P&YHhE4-=qY`=H95D%-ToY^Q+L?& zP@w|+qXY+e8B_dtqWT9`kvrs-e}#c(DY>xy~>D)g~R@ z*6vYFHaR&0!s|eP@-I6GU_pnFWZzq@ylgE+=|n*(G(LAt8j{q*M)KCQ7&c&JVJiR9 zZioBd4lzBZy-@nCjE=D%bxLoeX507&hTHm4qdDLmT~k*s%a`z#v?Ckrmk}4+dBzBd zk09wmO=e+lJ+TSXVid#y%u@e?St2Ktn?FxqeuBwXET0y|@a<-6cg)|abDC96=l0{` z&*v8+LnApO=Cu3`PJZ{uJI3}j1)DkQL94Nyw<*@fuvMnA1#qW3=X$g z$og6!o!8l%LA8m%Yz!PC8F#B_Cih*xl)-g(D&g$Um|VW3UvbabFWTN~g4d_M8|HOB z_nJigH^jV73}JV&xMe4|zBxv|26mXu8$3;ejaCb>!wKMlMFl%?!NUx-``vzooeT+RB$5eG*?=yv?YyJQCvjG3@Gw?`J6dWjvE! zjfS%wJwZTcB*_27oV23uD-!4p*4Aerbi0!@RUz_XT>`*`JAV&PkE?WgJ-%;OpJayg zxTb2Mz32DiN?R}uTC>Y;;{M15VFJm7?C{ubE$C@v z-?S8TtcZi_+G|3#kDJa)DqQo+mL%kKqhB$9V1TEwXlIA$R~TRGv2az`8-dSHKeS3r zf@paJ4<+`N8>6~?wO?&}@d(ZXaBFJ1&Qgxg`wzL`bC+_MDI`m+&C}K`lSNEgl;};m zYGJY9Z^xKfCffDL{m6hqbAa?O3e6S?Xdx<8C1LxUhx-?NFCzipq-5WQ z!9z?>XU$e$Qn}j-qgDyfRga+8?-uvClj44W2G*N_ugdV`h~+p2C|~mt@P{z;eg(sc zs}EbM;oD@^^Wik|5An&&Mm^@t?)9T+m57KNUROI(%;}9q?=7borc>^(H&BfZl(@|H zVDM@xbM7vQrV_yMxY*c0C}#bt(6jAeM16Dfp3` zUl(C~ypwdAC%*5Vis~V@VES- z-szgOc46E@6Er@z?wK@gW$nDeNQQC8Ht`O`*sbj@!NjKH<{z+GXpUcbUQ-DBF!N0( z#n5Nd_7ZBT!3R6+@QM)&tIzKiXxQqc9V5*fb9$rj=gI8OKd(IK?7<^9dNlQ|0yKAK z3lCpkF#=-lXXg}N*XU=*i%&!GRQScQnR#$bHt$YW!_-Sv)gC=j>6Nm)gCn3MqHyv7 zu;OgBsC{=31*tO$a;EjUch;~{`K@|-&$?9_XCQ=+iu^c{2TK$cvo*fWvGsyos+H{=Eb0SBhKD&Pj z^Xl*GnW{bn?aZS|x!donA%K2a^t9odv@+8yH(%u?GcQg_xCLbMSTgZf*jF#)D(6zp zv}pM;=oJa@|MU|p^VPAdU49b#08AI_8trDYLiQ{zh(L~lj%TTl^i0`}jymKHmHI#4 zxE_wgo_;=?8cM8^Axx8sUNB2pO` zaNs2UiLNp+(V+(bh&s6+1=os9CHMlLDK-E1pZZ6Gwk^ht%(LV{NqXMLTBcJ3U;e?^T=iJa^jXIhjpEspB;&yUxg~-jqUflfpdvc^fRQ9L9xcb z2n-WjtQ;t(>({_!Ysarrpc{?lG)NpF(eLC5#E|N@Ib^msTdWpnrYKFYT6t`o^cruQ z=zRhHf-p{4n3VF>0q^drpVBvWNFa4&O5V88qh=wW$5BWpa523~hEpP=URJ2zNr*1! zkRTgy0Z!*Kdq=HA`k^~(SBX|BtBUvCJCE$J3WKN`8!?BM9_T$l6)>P49{D{1fW%{R zi${y>!v`^aTco{92t;RhirR6Wb2<*-rm*!K&2ax}^0de1vCdfs6ZR}p(08;jhf)&^ z|GUl@NLYhnUsc#+#qLZ`F9>CLoW+fTPa9I-TmBB@t2-7aWH&D$w%2k@Sf0CoJ{x69(rc9exH(6Xn!R>C}=|3KF7bRCu} z^@kiUPsK{*s_{47LO4)(restKQvnfoh2Faies|KnB@l*2slGwPA!ok_K|cy9PxPCb zrN{CGwOPl>$;sr6HMDF1n`$UQFyd?}h@Dx~n-EN=(ik+$yRZjYCT=23Es$IKY3tsi z%ZzQyCQsOjHVXROxeVp(#HNmp^ z^m3Gr(X>+IPDQwrK$uReyqqRSmNQEfPb? zc|9JE*T^eLqCDqlEcpk_t%MqEUWfZ(4ltH0K`Aw8zIfZyetod4 zFkPfj(02b+Ef}y|92|U#QGLBD_=Q31n`C$di#|_EEQxSl@H8-U$=$O#Qmys0ZFYNn z!)#O?99z}xB`uBgX5NpE^P|)a;BYzB3R`Qr|7+An(K&@gCcX?zX*iJdnOK z?dHoc-3;1B82FjDtxagA1R0@|_b^0*j5wD^*AzRLSYDzAud2nD6WhF@f zmow2)9a4h{BT&bOD1n?{vK4jQ6Nta$C;9%*7*KN6AE|dD z`ZK;=J8V_s^>sWgwj4YeX1F=!JbiaJRJqrF=Jtn2)UCG}wu2I%2FU}m3>H-g}KglELO2aoM`~+y~{x8MCmUJ+}hAo?@ezR-Y)es>m zKP#7w34-52q8V)Q^)QV^<_L79(rO^8piSCu~5W?Jz&B2Xft)%Tx5+izx zET-*hL&*F7n<53hGi?#t=KiB|?D->3bNP74A^yh*_5oAEC!m+|EPAC_y^C)iKy(~qQ1=?gQ&${l5aI|x6sH|xq5U*|qecN-SS9w;Vo+3G415p{RDD%OsOKKMN? zNJvB_D_dG|ng}!~q`l7wc{F+d@z0#%a5k0HyEp7RmO>3Y*&Lba1pY4A1TOv-WOjVO zg3c{I_-iVDb`>XgP9AaL-21ixr(>!O6U@=Ax=~}?l_qC_(jv;k!NmCvnFjnLxNm|e zm|JXmqt2l#O-&o6O_hh^y`Um8Etof?cY+ac7x=FaITuI)(wS6pFUy3!(If)miv4g= z>tTRc-W~7oE#4f)FJmfL^wg@nJ;zs$J$V6Ur~64|$-D6QY6N%2( zfHx!mCV3MXUUJe0j~wznh9S)Ffw%Z91`eL!@(bdCM0&L|@gT1(GIFoI*#CCMZ5|E{ zM8(9K21PJOSXUZ_#mP5}t~4zN!jB-pUgf=q1_ES+u?eNR)?rIJ=;ce&nQpUC zqo%I;uivEGK9O$cTuB zXtD1JSbzF=172~nPaH+eRhWG*1Owa!nQRJ+QNfB&%Uch{1AnLB;NXdc+Tw+H!@zOU z02+S87l!6Ph?;-#L*yp~d6&m^Vv%BIddQwmBH{h4wF(nb$HtSt_&ky`e$TPJeupBr z_6}O3n>#Y`Aw_vu)Pn|BkSO8(HBr~m2{UuoO`YYGDu5X1+>Zp_Z}pQ|0OjyVv9uwV zm~}3Ihb-{EfIv*TO1UQJ-z?*AOO%NQvS$LpiE#_KjkR6rI6yrl)630fRo1jW4uy;7 z&ppz=e>48%YaFjBqpu11#K)8zoK@ZSalGQUv)zll^Jj3ww>2HC`!~S< z-#Up20fiT#j$G47 z2r_-}=O&+uk$JZ5|%gN9Yg(y?%p()L)U z>kq*Rt``i$e!3bIhQ4#K!11obP+A&6g4GQ`hA!JjBz)rp1|5#en@@!+6q%7a$oo+_%5*_Km?oG=xq0SS{ePSU19ucQg5toHKi}JcFuTb={@m9 z3FucUKAm#-RFMoo-54kzR$Ox3U#pH^cuCf7Q3B&%r|luJl$#&N7rSbT$5O!GUOHYK z2jyn%PI3wDojb-7^WG8)Gg5Eg4-f&esZu|sDY5oAlw^rGFDaw_sNej3j|`j-SbO6? zZMoX#q*@W#Fa(qg9M4b=M(vm9daGY|uC=v9@sMs(;D_TIZ3 z-f2X}#<~&Gc01F2bZmNV!}Q4nX1LOTz17w2e$nO)?9nKOy9mEW#JKyVZsIUVyC<3b z(|`!YpcvQ?+s7BDpX7J%dM2sGR1yHsC@!ezKd6=*$J~_EQhz@CW7UDs#}|AXyFPyn zAi$lcN3SurYhwfUr_oPaH)5W#gDZJ}5{v_Mok_aQ+^?vFCBk^Q5lsi(AEg|otW;sx zBI|zZ+vPbql+6CV@X{*dx+L3ziq!03WGfi%DF?I^p31Z+dE$D$rC?`0?%cA~{#`;4 z9eG$5qC8G$UJ1uYkS`F+=ADo6?ngojXK<7j#gUsty27TD(eMhR#W2NWQ-4i&DMnu z0l}MN2Q+u9We&X8Y7ywZWUm4rY}rw1eEhdhUAL{1@vfoV1mcpBO@_6%RFJiM9c?~J z*jIsJ-P^Xx`%51pG~qkxYt(5teqljGc6rOARy0yxADJ`rpdaCju`$T4ZI6p`edpM* zQPAOjQQtn$W=d@O#uxDt6rK+nNsXtZsx1TMTYDR!l9W7hRQY&q9o1Goey!~P!6>I{ z4^W?~RPW}nVX#EV{n}rvw|i5+C^Z+I0l|r4$R>umR=K)F1Z9(qkDgh56iLA_fd$@}oek_N=RdH^|E4vpbuO7p`t#wStw4UKb z?McNvjm>WP>%@(f+N7PSH`sjUobZYZl3IAQ9ZSE?`Gz=Sd07;PdW!A1yp|0Q< z3bjs{CC=vj3kUv_UV}xMFC!XJxn?wpa3s7#h0b@xkw(w@n5Km?oqVn+9w+5^N(p*Y z^en)YI8_nMquBDiFc?_pm-!k^)SJ)_dnl2fR4?2!a0oTA-68^l!K7gY*MPfiHm?<&T%dA9btVhrU3K+d>1u!Y( zJsejK%@xZsGikN9d|(rD>kM2lOzO4a-(Pr_SD{gI z-NizD0N1784{cmLq8n9{52*BDA6%j|?ap%D-FZg`_%mpHQ!D*yMu)HDp%_o8uAsfp zc5m-`{5rrphsAT5|4!BD&kCUF3@T>B=#g>S98rMfbqhh%!H~`r_zXrP{jH?eXnVy6 zw{G3>XWvn>+zV)7C>d301Tp;SFs>zfqf*pQcNH?8^A499OFZgaKa+>kX_mv_((9}D znwnTSMbQ4CYN)keCFL7Bg}3k73FpsI5aZV_F~&rS3s?Toh5fsMA~|+Gu69ruz}DMM zeiBa9%}xCuj%UlX;u{??_K&S%uLtMB!zw2la#%< zD@`5U?Vz!&Ew(S%#3@UK>u@Wn4CGh-P2PaPskT3nVmS8W?c6b%DSf-s!3peZ^AKOK zXhG1OW`&Ie9@nPwH74K(ThuL(^|8$#Fp2wC+LU+cxqsbZ58UzSZbucp40D#JwUi1B z-5E3!mvp0j9DO9@xfmECUw|V#+QDYSRn_`9sh0r!8c)yU`R1rnt~aT?KQAECw_UB) zi1~acgxU9G)%S2KF~drC0k=6HsC6Hbm# z-oiPA&Oi7HW&8MrvJ3pG!c6)DTy+=kBLByz!xuGsJG@we4dQ!OA5~&~LxQt)pWdnu zGUf`7!0hiNdWIPGWN||=`4E*NQhWa9zZ*)sum$4fUJ~&-2KC2b5Z=b(Y+0SlEv3fX z!F7UXk&$5S_G{f}UCw>gOukEcE}CRKW}ShJXntZE9~r^vI1Z!iILwsZHsmaHBpkY% z)lVZ9oLMLeL|NFZoh)DQ+m1+lN?Dwa!;SK`9|`5Fk?}Tm{WI%VrdlGET+9LO zA6E3t50>vTO)!g!0d%FLTuJ&kBSsM@??C7uggUa#rp@a(6~|Cw@7E4{o?PbtJ~jf9 zA$t!Sge2%GXxYJ+BV-MIGn(F@%58GvESS7JFH9o0$1505Vdjw`5MdIvN_}59M_vbKOAfOL%BRDD5&aL^@ zji74Ui8_+g**4>~|J?rU#)LkPyS6$lh9}57PRw?Y_JkJ1{cD@`-b-*vvaS?ZN5Q*H zFFHpoVyiXT;8z+m9jm{}^FE)0%0zjr!4#@Hli$&2zb`2thIz&wV-Ceiqz>}>brS1V zIW&Ab|J4sWcM*q^x!4HtRW_CS8DP5#<`A{m6FWEUG3Gnz`kH%S%R23vh$WPvtLBVm9c%VVq#)cPO0w;Dm~Ch`R!nUp>f%z z$*W5t669%$k0J*}s+-?rdr+i0FsVAW*ka7MRyjej`WHPL3ME3v$oQtesd`!6EVU5= z%r=UIcZ;*PPT#JIk)ojVql#-8i{-(glG4f#^aq!=jXWn>VrwUM3~w#AJJ~pYg}Til z1D#T4_LEtci#K6(u05HI#0gIvyY-2LuYT;JqQMrUTeB%g>U%$&!s3=HfK9Gms?9)y zxiiU?rw{_cUz9{}GrsIQv?!jCfiIkXRc8-qvH`V72;U z@oH~cU9&^3XUvoP(X;*6hP(L+)=1159 z%B0tS3B{HWufD#Rbd@D8l38c?yTMF+X)%tR6z8cw^uLgv`_<;x!z|Ney2 z=q8_jyvv83gGQ4hSI-sxuTvu)_anFm*Rd-XAl78dl~&H#?`qj38yx57GX{lo@`K}Q z6O}(vUOT$T8(+TX2&}yJYbOQu+);OVP}NYHA@RYG2Zl|wDePa#4!8%9>Bk$siQ+H; zW~FVV8wtxZ&@6C+q2D(8MSk8O<_mUV(f<^_-C_LDOxRRw)k&Y2 z3mh0^T2z#Jc5t|9yEf7az&tEKXc)FL(s0UeZRH3qDsp-)mt`B+1{KKqKPFZTq8 zx*kM3CDRJ!_;#JbLBp&)l~oF>*!qtKygKX5Xh5vEBWt~mKx(yLYS&pF$Y1^GPur7@ zvHTPA2bjrjVDc^0ZWmOx--wQ)g^3ZxwgHDzvT2bu%5;N*G_H>de1Wr_hI-4zdzX~P1~9qm4aFj4&Dd?hpOJ(wFWu@Z2yAj1x#`NpH)%32eiF! zUQ{SKZ5LlM2l+bn zQg&GnL{UCfRetO~tukFT)D=qS?SP>BL>}xucns5IPyhvZI6l{AFHNB*GPGg-;!IJ#2f$^nYF>CZi)4h|x0*k9(NlyizZ z^5IR8Ak0BCHzb_JIs&@HgxjRT> z&H73V4(`)kozXj^cKOs9hpp&yo0hBljNRI-LrRChrO($#J~&V5Wy0)C_EOa+S*o6G6UE$cXXqM;@*GFIJ!V#Pa*Fme*wFqx@uouFdPX6q5 zENFP$UC1z;$6RF~Bx(sBlLX|lTvDeVleF!QW3Yr7;uK|ibfow#r9+W6D+myUgoISb zn!W08-&&H_cfGv7sy8~j^6c!eI&;Eoat^@AO2CTSW(Qr<2C@xkP2$POo`StHu#B%{f3bGAhhhLWOB7M?GF!} z;#Y^a!5J*XHbQqdBpL9Q1!50RE!C|cCfD6xzt;7$CoW%G@)MhrGX%2w04k;-OSq4^ zKyKq9SIS^dxt7U=v!anFHbtb_97&6!rS<+Uo`V64_Iz>(W0Ef5*sw_UQS`xXv2njV zkZdgYwA~48Od;PClm%|PJJVB-3TnfUrNXEzL~)~zo+GAJVPTuVwuxG?-RLLtEu31X zm_R9Z7&U>0t{6x03Lq)U$|0JZEA@&)`!*It_URRYpc7?_VKIaY!g9B@G@i&xw1iY` zI^J^Z-ID<7;xV{pdvi4cRgTuwo6)fQ1N7Wqszr1zGRWz97=xckt_uzG?#zJiT74ck zAU2y%2#NaITwA zO!Ig^2{wMbOrg@)(<*o*d4lK-nJ8`KY$O|AWzH5jks z3(@+N{bua7wLBi6;|s(}Z2EilOXq0XY`f{QwsN8t7se3(=tv!n{>!t-eQ9U^{1p8` zKBin11J(QLHf-n;=fND{G*-MHp>Z#IYVX!^eFU1xx~k1}FIwGU6x(M~jN9EWiiCsi zw~@y?`mWE>EysQnzBk>%kwbwgH9lp6dW8;S#nSP{Jv(c{e(o>g3yUGna7luo693G} zfQsBdbwoMVjEV-DZDrN(SecRyrGXE-&vc?yB8tl;XafyH~%?*xi zVTWyXdeOBE`ymper@M*>6a5dN|hanvG-m#-KM+;Lc0e!{*h*jL*_%7#eYywWu3z4k(TFtfT?m6fj5xel`Q9HpPmWZ+Y&- z^=SrcIp&`7*yYfD)kEWB#!ed zRmUrt-`te^Kd%023y-fRwZ?v0R$bY8&T$x5GPBZ&X21u*M5~C7M4=Fk5pNW}kZ?t( zAzBVIO)AdqED;&TQR5hC&Hp9mY4bib+DM@<3}9dv_-&4$P7sKs1lY7p)I&(Iv1d~% zwLk8{fA^y@_pJn~v%~q{tFsQ9eAl{6nc2Jr?zniZxx!|-4y!UHX?KkBxNhouv~xm& zx7~t5xxN1sn4&+CUKLE<-Ib}-Wlkf!)4AhliIti>NOPpr26f%sI;WJX2cB<@=M92m z$2=#+103=#rz@&2X*LI5J951Se5i%Y7Pyjn7t=V*Ox&AoFRu72wSR4(nv?i+n+0)n z_bCwVbjPh!+w?>AAdk1@5fd-gS%1_aled4v4(E^|dP2fcyOt9vg9_v^-i6IiBPB9O zS;e83RN=PYQ^tjEC3#ClG1x4m?T2X!px^1#ykGv|)8Ci&Dz6TkHqO~|*=Pppl`n_G zgd@#MM9NXT-ce9>2!$^PD3!9bje(#35!l*m(lIU-8Ml&~DMngTC+I?IvGl;J4T!MX zE9y&zY8iPM{*ZyiKqd9TZCmQA>>$n+KS=!?5yrR=3+(xeANK`3d<+uR%z*bo|C+P=z zCX68w)_mq(-b)hw<+YDN(KL(fE(5jdg-))VKkqO>t5t@q_ddWG4y)JG*TUYVzeDQ| z?b}ak!acZ#sb{{4Ub|v7pi<0X7Dt(SQyRc%YdqF|!f z#-?IdnrJeC9ZRdB>ZMF@672e3(1CP0GcKdLO5RUkyK7kQiE!0s8*q)oJ|p~QaV83t z`K_BA(taQt(|HAqQZVI}8tj(JN%Fw)6g^Tp7^^YZHuh>u^{-T#hQwgd;QtuwVGN5N z5Fb8Oc6?XYv7&g-pjj6j#7au2bNX8~%z%`w+UoRch!vD}H;O5_gBq1=k@!0dG1I|B zdTb&!9F>pZK}_w!<7sZyB8i%bm9;UqAM_i1?iC+eEJ}URuU{VCg0Y-z5%63ChJjT&WcF3&4IoGMU+cJ;7h4?D+ub6(&rmelV& z6E!qYeixSL9wP(`gLTcXf>?M|`y4V*9w1K@%;6*^AL(gI%5_@Ox6a;b*~A_8gi_L^|iKyaF;oAHQc?IcddaG(!& zFyZiocuk&wY~+%O9vH+x_I@nh2~A9Io2J4;o?t?~h`7p&b)1XnsXpk#Bhv_9Z_SHr z4r%DHc0>Zx=TaKgep}S&=Y;i@NnB9-um7m;) zfD;0|mrPaOz3b2NWME;X4IZO=YNNR6A*(tqU`!i>Nw1_#>2FLr6-M4~`u>lOy(dTDZ|#u^JJ^W?5M zZGLn81km&gC=DXor@f%n(;P+8-hC;u z4(ovL!tkgW?=2PQ76>Q{#=h2*4dBNi$q^;gF-1;wOE zz7?8k^24tAc{arOV+YayBrcU=(HF1}VtsvU-}=;!$ER*p&6Gv&nzp>)Ne{(3rJ-`Y6SQ zaCqACB88y7;ZeMYZvvBpMbRrzWAyw2MzJ6otb`7O3Np$5)5fFK3a-a9d}?k`<2nHp~vI;e+YZa zpg5wgTR6D8ySqbhmmmqQ!QI{6J$TRrcMTBS-GaNj``{Yj>*SStzpAHltNIVcnW;YA zYxdc;)_#)sI1c6);hymq$ZvRKiC$r%r&}QZ%8b{oc$mwZzDo`@HJAS}x%uS>R;b$} z*N%>EjW54vK(idappMK(y~DEY`KM00#QiIYTgKrt+nY2{1vgZmxBmuz%g*S+6Z+`i zY!HQiT36n5+i=EKVQRM9LpwRsNs`Q)T)=K*_3}$=`G4D6LU@}TEWqBP;wdLlN|6G^ z(vPPOFHf$?aLlH|io}He^M$O$hKzj>~zXoJbO2;^@ zW{a3@lHwDgUJyRlHDyD+9DVL`3n%hTKs)RSb;h27I=P0L_O5%j=tPAOQy;k6qgQUU zYtMw=+1f@i6zH2eS@iT87r-`k@_|Ok=-tAE?WU%t?ZUzd6aH-FTr>`3Q0W_wfV_4s z+b?)dH!$}Hpw+hw4gqz3dv!eYvctIo9@XHD3d%eb0V`KtKond|0oK=09=wu`4T>7x zy!qVdija_W>vyELpSPjFKpXU({}+z>d9^%)=(&QupSV0Sdi#sS+lN2{IGCV%q5X<9 z{Ro!C`?OVu-9xhUy9&}l9q)VMF4qq9sA<0k1>85qm`JzN#pIf5I%uBI>-yUuw|76? znr_5SV`TciFRpkAb9COf*^-C$0wnvACQcnif9-1ofW@!i`_dEoMY&4vW$rV^=xn_2 z!+0jophvW3gVmC{98Y&cjKF-LqjoZHFin`D4^HA}o`;TxHmj2BWLNR*XAWwov4cCz zTUZ3@_w`1_>P>2uI`y(}`zlV^IKJ1=ao(9~n&amiaSW+$9U@+GU)-kD^ixn{9|Xym zmBOG@5{s`u220y-13!XV?LU3F1OB_uZ_dh$xZW{Fdy9Ba4ro8AVBB)qz9!P(5N-c@ zt5=Kw_Lx z;J-np8ahVDCPUO?aHc11ZU1~nEP-l7B2brIB$sXx$!WDR_IeERmU90;P6eW-zt)xD zvs}j@F}GhGJ5$4JyAk*SA;1P#T`>zeDz;osWfd?Dw&H(;KlQ0QVvb5H|L z8u<-<8Y|o3z~a3LbpSqrjm|qEO~wi3u>r86SNf9JI`_245~ffd{e3f}|Iq^If~Da? zgw<+Q8RY*tFrApGr4>ZA?C5a@ltP@$L;@h0^_H)oTm99{?^z(J(MAVz*n`w?ExHc; z?|%Q7p0Rp{=`H66cxG?UAV0A^&3MQK(ov%}x7)JsvhV!f;~d2;?Je@%!a-j5FDix8 zrmT~^a@j6jrK-^C!tIcR8W#h8L-Ok;Vz6B%-#}zuChvnaY753d@6Vln96goR*9NoX zkJ{gabfG)Cn=4kU@6T37x+m0-xqmM=LtJvwtw_2*h%&xWE8x-(LU71uBWUC zzg~RmksUi*;hy0KwcBahzd(O`#~Km-Udmw-?xtUDB?P2fB+I)O1TTAVMM{&g0<{c+sg)J(^y%DAHP(aN^xOnw#_>+f-G633@UBl?vApfSBj2OD@& zg<1XAq3&L@9FQe2fcOKm-H)Ukh(r_ntJ;MExLVb%zs9%!dLLz6M^Iy@41k~bCd9pj zY|qJZyROgHk9FV2epvc1Ue6O7H~0#$h&NO}K}2hg`_{e*-h&VNxvj5=iJD|y62FaE zH~d!!XjCf{zpd}!!lnFVB-X9-$n^pp6ZidfWmOFkpR!?hkUmCZe!_v=*4fcP*hwI& zR4KMQZoYEIF0~K+HI%8HMCL2$#_cB-Ie2&F{8o3AXtD7sc+5{q{cZ;XabEvG#@c}= zjKZ!BoBKOJ|3s(e!!#-OH-U?GzKEN^ zF4;Pi$JI}o85c*?YLN9LPF*fP=XElfDBa$VxINqGRDeT}&k{I~@0R5E&uOPNT{%SN zZ%W8xZnT-=vQvhL(|oPoIE&HTo>QXI?lNHVuxmOkAKWRg(APyZT zqNh_Y_b@(`6!d_UagzWAm8R3-6PdZ9a>--MG@hp=Cnw!y;$<^~eU=fyVvxt#cRJ*8 zW*4|w!O(hpFrpDalN?hFsF{2oh<(1p8)Ex1FThOrqmRc@QzCBhHMyk8v7Cs%p)@VA zR%YY8(sJJR)dENGY$To&<BP5;0Bw9WwiZxwTMqj0Whx}{!%*8)v$1>epFHQK&sHY!E#gks?J;LT zEKy5YgX8$>(J(3|8+G4X1}_kYMIDThWHftK4ygHh3NhS8=rmmVbTNTDppEA)qnQAVk^`5{xu{ z@|_Uj1i;sn<$NT=$?IxTwfvdHA;r8Wd40dJV)>q*(PDg?hbD+3m^UIM;5nJe(7x8%M0Ql%YOR6I&GoZG=7OHD6x63>cMB`W{gD zdH983z zm#e_8!R@dypmMW)-jwu<49~AC$s#HKEsA ztV_bBk)8(R<3exMnf_ip8h$xW)lZ#q-|#Q}(cl@HhIF>OT;DmJsy_%PYU2i5jZ(Qx z?IX9j1Z3bjz4-xt;dqIIWHYvu6TMi3?Npqh13q%|{HF=x7h3;UaG*QzgB?d_{rc{W zAas$w*Ci+ROtDJv^oGy#w=uu#eMU8m)~+Kg`9x$52pwTY#^|p!D7m%*Q-)VbmoCvT z<-vxRQmu!U`p{B@kfrRk9Cno%o~kKOJlEYv7B5c^;UY&XHvW;X79;?Y1@;iBQaYHj zxCM{#sA0)a5iiHWNjk+_-QRvFcLSNHKiES60eynW3~EYvR+Bh}toZV%P2mnS(Wzbl zw1s(`Z*^kIpRRmL(@~&^7{r5vK=6r&vs+Bm=4Wx$SuD;eRt^RHv3N3! z%>7^d!av11Yf83-@DP+kdP5O3k4X7++P$cV`2CW0P@zguT8SwTW>7L>T(P1M&Qiq~ z=g-F+W1aTsFAQGX8L4&3BwCi=35HllTzB8%Zhwy^^cKlL8Q_Sx1RCi-^Omh-bE68k z5?~J8kM-k$rE}YlX{wwsIEMf=?wJ>u`{YR|u~7(^`>E(}Dyq~Pa=V&&5_Eg`DAy2( z*c8;>sk)D6I2zzVQd^6!_<;{gBpGDTuxJ?6e5WgoQq2Pt>*k$JKBn zYfBD2x;tCZX`H5aw}ma7cita@LO7_-_jHKZNvyH0JNWpFNahCg7$sd|5A?Ejo1V9Q z8;*c{Chdsgc?P6KEskg5KFa@%;fQmE^@GOTR6{LE(-8*o#WaX0aq|l zx7z4PL=X8OrC(ew;E8ZXHsG6p1t$5CL7j0A_gND`h;wlj5qU5vS~WW)^7A=%T`<7b zH45J?8z#d08L=k7CZi3FJh8g^HqW@^+Z>*5O{YG7695D zJ5!it_!Tai!Vtb05Ch9{c;q01fZvu*dv5~5Ww~X{Vrc{106^(8+v~FZL;n{wUX0(+ zTqPNmbsrBKfg|gWEAW4v^Tru?W>y}4uL<=ICubqOJoY=1W2F!q42C4;c#+i#q)h)m zyl$_qai=h8>&0wf@5pSV4YdT^W-JQ|p4Xzy0Q4M{*zk~Rjw$5v2vqa)&A%r;oNq>z zW7UJbRfW3+3ZNidxNR4yRAZvqLvD<*Be9-A!Q$>D%}p0Yib@Mh`92^!n}7oHV5RGI5I-n*+A@T`zR~*%s7iMa!(yF~HlZ>6c)#+q9qvd;z_L!nPcU0go*V*^aAUjW# zOMd9bpDu@sI=U#JggeLs1UQ+>X*AKC?C-kq30h@<{J<>Y?aXPwH6wo;xtb*_<2iKb zPFAo2Vq`~YY~RKw8XviqGV^mq!P=3f6|E~`IOX^i)I@e%`pe|uTZHvNw8 zDjaTAe!q}Nfam_MwaTg-;%*M`JXsJgd7q-rGx!Json;@oLaJ0(Q%M)&L|%eIYi(;R zEJSjdNlm$++g6$W2=o+uc0Yy&eK>}6ich`3EIINo&Mgiyu_HG(I%1mKXi zpPz1tHNFY5<~%JO^wx`mKh5c%<(`2 z6g}#hv_e9iuTLLMAAnK!nNq|sdZST4I#o`G-^=$sZv%2+ok6sj`R$*8)NLX0+LJQC z9_H+%qcb$SSc;jOK9y2tfD zB`~g+q`OV)uO~ot7w~(qN13 zruiZ2EcZu<_F>11x8@?-)YtmnCm&SO2Ad`IZy)3`c*20hu}wSg=6b7D`o#ysYPGPK z#^tLf2bnC$kiFLzm|+iSJP2~5h;baLSAR{5;&v?JWGUSQ-?3!9?`yAElqxvI-W^s#B2`~QYcdZr{xf&%U{YeDau z!tb0XC2XZfGc@JV@6%X!KmND%{B-P<5=Il5D`+#_6#6|0D!&$Jrd2g212UG%6WgBG zI6z-(xuY78^H-Dn*`QEnF34l`W@y@#>x&_a8!W0yDU4%~_hkQO>M+v)vs^BGPNe(4 zfl?+ADBb&IJ~lot?!*9Xo$7KogMF8{^O*Z%iP>$ z!X`M3_r5XSr9HS8!a%ehk4g8Hbkpt~3AdX9inl0S6zXnpI#SRbEOqYB3f*$#jom_x zQnJpMbLTzuO=P9jzD_`|`1rxiA=q3r?y^cP*sU{2~hDt8hDD#(+BOT+TaarwW{c5uZz_CjH{V!Vy39%Dwp& zh+E%3rjS*Fudg^vh$Pk1c!K%KuWupbm+*YlKJFV#@xklOHA)4y=6W0v#|P8MfZ&mZ zjYYmxZ8!KF?mv#?2Uzwkz351>bgs8TYQFVWca5=K$&FwFA4D_X-$&l3^M13|s+(M# zId7=+x@8&osJMVu3QER6A!@y_W(Mn92R`LOgW;O<#H&K{r2%Wb^TIqAP)70#yek9L z!SX!lsI!%3EYN9Fj(H!OOw+R;mF$r3e*na#m%B7U7(sWVqty;cQS(MjsZ--I%@ z%GQNQgx^+2J`y5wFR%OZ4`QvJzx~O9Z$?0fM;N$A-w(FuSKWR%C))8VIe+3f1xq!% zNTHCDkEQA_dr%uzEu}@;FeedqOmeCXfW{#A-<9b{Eo;{H`P*Zpt9i2y$W685#h(zs zfw@CGSm zc3^(Fxfa;7sj@|x)o>5)L3IRXV}YeZ7?M78z53Ea-6eRT~f^4sugLfNfRG6mYCB%x#MP6}YC z9MYLGZ=$1ce5{9}jw~_HZ2+Uc z?Ah;k!=(;qc&$;!ra7fXRMm?zK)7FGTfc8gEx$T!)(g4x%41o;hSVY5ly?E+ZoXfC zOtYVshDt1&Nb4bQ=izok>~TL@&9sMYDdXtH!Z|u#WrAf_0Lbg^GLjIyuFslTYxbS* zt(U)SMug>HNd#sw9%7o0{njkJ)UNXo;`fia+GU*q?Eppz8Fw-E=Eu&`8hGXFGCDwK zhIabmIC|65s|M=OoelWeu$HKyv903ocq;R4v^p*;gNicRJ$&OJ+gGYTD%qql+4%kG zH?VUzq%>QZC9dUQ^$wp8dzjf2-pt8D1cAD8IcDu3K({XkP^g&c7JokKPYu(wuW8HX z^#v1-HaBTpR~oKgs38;CXtGLo5Epj6vT+Mn$cJW6@S5ak2AOR}4xdRn zuHeZcY`kudtEh<#N#9|_vL^JcKn3qobdU^&Q|14aC?sA5ozclQc4nRBaei(}4d^vz zaXaWL=J&V<*=I=Mxgyx<>hmgV3p9``qY5Uu$_k5+564OY z+Iu`75$`$EaG!rWSCwqOu7KLa_}%QBz1kY4|0V|OPVg_)LM|muJ`5M&fS*g?crFzb z06f5tsIA5a>fgs8!cjj9M;i`$V3%tAP!Hx4T#8{W_3Iy|f@C9OpJ~=Y&Bg^v;Mmr} zk}6AU9>1oud3w#;qL^cY)9ryKyd>|F5~YnAZxFpkac#w*Z%@Fhc&TM#3ZeD#hZ(az zet%6j_%3cBAh2F2lgjkSvpCmj)?93qbaehwYiZP-Lv17phh*c=-rN zJ2#=DK13PoaG(Obf=|-8Amk0?GyZZ%yomjTT(jQnleIHqRNxia*1;ftQ(kF2DX5{Q zKzIk)Vu3)WSgW_(aMVS9Omid@RgOlp(c~C>Yj&&WoV-zY#PYpAOc@seZud{7IZ_X3 zXi&4==zPn{^F%P4Dw6Ye64-1Af*6lY)6s8`g~Dph>JgK_p~+&_%@a%4f%n)UxYj!EE`O+$r zI9O)Rc(hWH-~=+sT)dBM&}ewsjpbMo%=CvZ0%~|Cc3~c+I-F_VqmuJ!YfB<&Xt^szWhAKg)EFmJ|1tKc^LJB)Ak+d#qIqeMs z$5zC|3_(#uO(4%9B|6Hq&w^`RYF*X+I!4v3ll2Rfy28%U`keg(ByioKbbEgIr8M(| zs;Hgc%PR{{77Y)69OFVBd@yjbSf|BD>EtUsj-OK0ey)3<3KWmFQ|hTu>S>B|BS}Yf z1`ygX(8a7+ zUUx5)u({5i-o>xwM8vbzSff<0y@Bhd(Q<)uQ4s%(&wHMnsOI@aXwCIO(3t=A>EwJJ zGWZ3Ma=SI`eAYAPbtG?cuL5+(Em#avnRSyI&LCiXL&H?GZgbzUwBq6+casVsr!Z@8 z*3gQzn<=iVslK;c>j5YWTVb|nyU22{;$=NHoLm|%ZOZ_=o_P*x8u>Nf z$%~#pAbys&&f25D@zY~2*znjCOArCuD~YOel0qzRXKZIMc5Zb6)T}`CkMY zPof|NTsL=o;d|UYlrxJ2+U%c?KZV=NTZcdYN*pv@4`SJ!%zg6|koD0Q<$(pcFsL)n zZzEWPA%6U!UlmDw!(qIy%F*V{zBkA0xJ~(y$N7`@WvIx)_ss+ZF@iJR$8YgtE$bx3 zg0Aqr1m2uAwqQEP?7G%MM+XF1%u>vnr~0-^%|2d%L9d28>Y zz6PrfX*=D%aX3|*t5j4fFi(tv#82P*M^J~9alnX+Yv*dm3q!c~`(>;JdUB}>t#+NL zimjLB?8H`Yj1JC~dSC629XcOVsy;y3~ac0vXx%zliKa*8aaao z)7+E+5&PqktjRh~H_k+7$QR+hrt#l_4SH)~%AY*Xq(UkuisioA&$)*7nWqorynWbz zVH2<&FxOU0|FG{mUuMdGE3c>BJ~F7{7L|VmdUW28Ezr2%K1im~U4<~55s|p8>Rh*y z4G5cKtr;QsnPAl4%t1kuj>L;4Lqfz;1fpdpiM!-EqKdgB_sG!462d5 z&v<{j(y0LiE<}W7mwYp6vg!$QW{WwQpq-l|jx>#5Hzt2qvc%9 zkJzVx2&2G+JY<;4tC_ci$8(DVudUYnoydbDPiWoG0O|Pl9<|QUL&{&s+Rc_b-26cb zg@k?(C9@96{!N%{!LB;d||~PL1}s<+TvUC&RXh_fYO3kU_8>$6fx7QY1vf z$XW03IZKZWXvL6N4_PugmEDDa$S&@qfgn$b5w{AE$)5p-4{Dv@WJQo^;V{-Y` zb2;8-Gn~z|r)dJp5Y7MXrTXTP^x@HD_qKRrsDnF*sHdaB`o7_=!Rcrk>dbXDv9Zc& zIA^1EA-ttM%uhhX*8t>b5#%*2u5^-ecIzdv$=cS!V{%oC=YC(5`XxD@j2;|b`5Aev zzES%?oP^J$36n`nEUHhd0eS5ms*h~1IC2kBkN5dNjJ4{)oy~2V&$do`)1R1W%b*xwc9mc&JHV@y_jagY$x$SA3dlyRf{}zAu|v+3eHzcZ12p{Mif>3gY3> ziU*>OwmiWWlhQs7c$5O((LmDB*kd_*q9KPXYV^dWyA5mEfTZp=r_|$e`x^%<`L^Ct z&Xo2(VC|^k;~*5$g>7nBw&QZvV_X3g$Hj#W8zi@?z)938Wip#@5TZ|SewQB7KeSyZ zvxZspJ1Bvs-03M6MEl_C;70X?6VmYjcV`!MVxA`b1(+!^K2fwI7{e;EMOO44qelA$ zO$4dCGSP%jDNLBKw=aU8$C!3gZ9lLr^Ta*oPc4@@YLa2ruff~zHKv0Ku*y@0nw{T1 z*Lq~Q(U6Lewc4IK2K&{p)2|?cY%g9TK)FX&)raFGZfUCestH1$Fj-3Xjp@R%H-@tZ z-&m_$+swJG7Im@}BDAjiagKew?#@iqH^VU!eC;8{U{}jahvKRjq^K?&kPYtuaO`C} z2Pz$9upOw2)vFK7>j5L3mU6B{--mmbCw!q>apd5Xrq-xjWjEe8=c^f{;C$%*YY#)x zBq1?x8tqo=#<$k+Wm(fo1SFQe3N&90 zcf6gc1>@e)I{ub}Y2(~b&T=|=KiTtONq>A9OSEx#>(c;b2z=uIf?~xFbRzZ2-$hP{ zc-$5+S#sUPQTM2nuvH|bft53J0sKD3q<|sAz3Cik8#hSp*{t0Cr2>7{Ra?6F)7VnV z)q}+6_d|l4S)=QTnP#hBVXVRU2_vlLpA*;$IDtW_ptdzRUv3RG5UDUAlY(#)0JB3o zT>^%BmRz(mr!hRFq3{1_6QDP_iA|9${35yEnxA>Tn;O3`}B2WtsJ>FDe;`v>VLzUwjqt+(u*?ZFk!Hss|7cvb93eK6*-2 z<_R1Cil53+!mTdWBorYb?~Y4V9@?{flij1Z>vD}&LlRo@`#sVM)qA?EKK3n%xXyPO zD~881)zSFPe)|wA^DC9RllT%gxN2%C$aQ{KUL-)kZDmobvr}G5BHd6#MrQ99#=;uQ z^BoT^%nw3AxE(YxJW0k3l>7li72%Xul{sG}OqitHKIFRd!z(|7G` ziSP@EI|4#En_#A;@XaxkRawf%s+K0hyWjXKuCx7LbHzO6;#I6d_|B`KRAEPm*SxGJ z+5kpF(mBZOMm2e(!r9&a1#GTic&RkL zBz=Y0heCx^C1(9*+UI*1SFIgmFg+#wS^F|!?1nPM!&)qVP|oHGe?xH zmn||BxG+iBZO64uag^=AgnS_`rRycLybIdJ`see*b4~0&UM5q?7~nSc8juNy>jEN3 zUrOZn1)j2+4R@?OuO1>t)xb$8F8Mg5J%|Ig&rT4D#wLWbnJjivO8X+e4iS8`2@Sdb z3WEYVsqP3vjabk`tr+riuY2|tu7?Q7rRY70GJWHJvAaEv!$= zv2H{@UZatwMRzH9<=i`Smr`gz0{TO<7Qgt-rTdi{k~_`f4(jn-+k`<%MO^V;bRlj<3VDLdM=-Y4g@nO;TVUL;xNpR z41gX?N-aG({prs`)$m31yb-ZbJyqg@&aYSuDTmT#JUM{QrFA-^06~@_xvK}Jgcyup zZ`hbBWOT)H>!l=C&(r8pCuIt{E~W~^v*C*GR;Z3vSF@HU&+GlSRFk&E-tq8*X<$Ok z&1;U`;v*od%Iun)hL!-oMF;_|apqJB7*cWq^?aH`h?HD69|gcSHw&Fzy3j+>_vyhXlzk?bevYfqbcjW+~U{xuSC`kKqckl@5;(gb8$5vmcX|jwll1}b?lpy1L_i5Ug676 z2xAKj+=F!IH&jeE2n-o|3<}DI`Tfyv{lf1%>QB^)4}2ULVePP7kRAp*iZi+%pE}x7?L&pe`T^ zJnaaG_(NAF1T2Cr$IdE#7^YLoE67lSxY_4LnykHkMW4Qu8oWhw^annLppbsKgze%6 zc(q}HZm2Z)@-jhpVzae$wWL=R=Z=8%hKg@zu?bL5calY+-sUezRx{Bv_e>5eQtT#g z(XDt6u+&KhB*L%+5ZH1~3Z|vD?iV(YEq$@7YDtaIrp!v#)_|FtAme<(I~)k8Mw_*Q z4h)v#yFqW*yjCg|P$9ysNY9TIu5x>c3F7)-Sc!?!2oHCc1+)KlF*E=d<5A58gw3@4 zQFO2R@)_h_a_#-2#hhs2j$ttkpUa}mR^HoxiOIt}{1x`s8b9p(QoZdWQ@HJuQ6B3$ zj&|rX<_;X-zlMXE%tpX1$AgIq@S#-Efwc1k*2LaJQ}FSjT~4i3`74-8z`tKERtf88 z%cm|+4Jx?GM^A|%elGomCs`Di#65p=#BKq`(nco!IYH8M#!|8KC$S8i(s>SsUpVt@jpP!d=P#O;2UZlcpT>Bqc=Dr10drBS`M9<!yj(hwr1 z04>rpM6N2r*(GhRKDL;bC;AHa)QC4;x5?AXD@;Oq3Z*zc)jhD=!}h8wl8=N=JNy3Z zj?=hHz_^58mZyim%JMnl&UKC+*ZG$@Ak^~`@ra2w>%q42v1uog?g@KJpwt~z1PLyE z*p)$gFRBq6U%Xa?e_v!0s^>dQEqxiko-Wi=3GO(z-g*rRb&e+Ydm%u(|DZ5Y{nEe9G>tNs2Dobe_5UZ!ShNA3QAd)6$$HxtTs z$q{}4)5ExrUm#D3mqyQARD%4O4`I1x2a_-TU|g>ZCvR-hZNBe>F+x(J?pra)s&!*< z@AB+I9z{?ZW!;bs7k|6eXBhX(y_5h6H^bDL3l(E5+^-c-Y}WGV`E8H;U=GLB%r#;# z>_BuDxn{B=3^+}MvLQlueWd-es+A&JM?R`0!b!~T5mDb?vuvCVly!h4lQr78g6e;n zWps~?&@n8xFhOsBWxYnfnQ}z z^zcMB8`8g$U)mk>v6X+4XHDg$ic7$z+Z|0;m0GYgC%NzXn^e}ooGYzwToOtJ(KgEL zV1E_%7qxZYs1leFZ-XrcL-iA?g5J-cnV~nDfqHq!E|xcp(7q5W$4{HgQpOa**XxXu<^ z%n%k(oflIzZnNh17w5GfA=#&RUWe`GB24a@LL?XLY7QlvufA%_d*a?~Lhy{&W6&1G zFL{11koV^LQ~gy!;(ttI{ppjxPPQdS98C0E(6fcRUGtV{?JerQYXg~N znoIfSZ6FdOr~Mpf6Dg6PQ4qq4G0aeb5(j^0fd$+7tVc9al)V}0O}&uD64fdCOm zr+4a9ox&1~cK>mKf3xW6ORUE+~7Btv^D=mIPzBz;p((~|_{1WjO76~*TsB9fy7{OiQqW6FS3Wq~= zygU^<=aZy8_iZcr;5H+L0PXxT9UCUf5L`^M8pqmKu~0rvMC$pPS3V>NjJEMW{U3bE zM93IE$Md#1Z37x#W$|hcVes4D2XLSx%Cc@(>D9{OMaOLV;ssH1l_4FZRfBPjcgRcbwI@8T!0!XH;rEew>8IEm*f}KS{ zGJB|RJqC-C@pn8+hV%EnOiM>$S4=g-zvq8%5z z@8#0b<&AjF5?h^0c=^P$A9&pwlg85&Dm~hw{P3)zkqHT#%I$oNhAq&Qz z0c@_(YLZw$jw~pK6EM8e6y)b|0MxfEIfPcgR~T!mI8I!RGbIOIKkbFSGfI?{v(nV~ zHq$l^t|W2iYQ_~X1DWSMmRT#yV!PO6R()~wx%-3>RqKDP@dI&4v4n?icNYw0oACA; z+5&Y4n)&#XtoJgq0Q=i3=#Qgp3;p%4>j0e@47d(MP$(SLoO?cGjSYdg8+EM5;+UO2 z{p3`~OE{{z->+_vgQDHCKe>(Yo@T_2D&m4Dkh<3{;K0?VpiVgs`?&coeg!kn-6xTJ_b+rRSc$@4R}X_k@kS1|KfVb0o=Aj?|{Fi*b|y&d6txZr=X$kG>n> zR-aCetEU;3KE$!)G(36AB0nxY?;DO-6p(1YDkQH&VZT8TWn?0L`3*mM76q`R{-%sm)*0=o) z2)m6r$x(V|-t5bWHj}^&cQRFSSUWoU)VX?rQ4?0rWt<9Zrr{sbduE6w6a(o}V#z(X zwmUzzHH^6k7&9GI%>e0*ki;DN1AIhyj`)=A9TM`|#R+2#%IJaq8^w0V;gKfv7D>m?;Z*;%Q}cmmn5=XQ=QfC! zJm@r1C&|stBy$!zc>ZkLzym#rRT4qLk)D}9rncdasXaeeW}!1A&yyn?J}@7C#3PjB z1AK?TKfZ&r9gH!t(NH5lUe5!aR;?k{00l`^t%w%zgU{+HKl3Bm@zjt+k{;Z=$8p+E zd7!5cxEv1#SxI;Y>%I%}JhTMPB*2=gqz4xMO;?DT=K|>pC$eRr6<{t2wWFW_P{G#h z{`|B?)KFg4xkkTn_8^Jm(`KW7d_)BK?Qf-u!+kWInl}1AcVM$JK0x$c{yCrPVVSu` zm2PGG5bjB|eux*7c2N*|Je8uJ?5vy;-j_{Nu-t2d90y`B98^@P@FsEd7tnA-NSx8Z zA0f%JcId2CkAO*c)`wp4gt^4; zt`HuVk;Qy!e*8|-o_VhWEyEcNGXrLZ!kG~fT)|Rp%k&v6d@CJKR#A6CXYeG5PZt3$ zy1K0hIAe~$og+xPwwQH;(0w-C$*DDg8Z6Tf$GDB1JQSv%}u{E zsKM_z1C_wkMA`&5e3`uS7*?*GWnd@?iy-++cQDNSsh=RxbUU09>ZT#qM10?|KzIZa z(F86dWEIDT-5WhFNk~|KeX!5GC>(0?7B7x+i_>il{^y!5GHpQ3(zOml00J#=?hWDJA>YCUW1l-D`KpdGG1k^4N0XRiC+V zeFozg*5cyO<@t`uwlyf#^Z;|3ANFeHPZjE*Elx>L!O%yw@m(Y66C`Ujan_ol^_u@C z4ikJ|7rS*2Cj^SNW|DYX!N;N3j4NW4V&+EswfSIxn)~z1;w51iAhNb|f%q!wnI`F? zZarL>w}{2^TH|Jxz^X(p*hDg6=W!W&fzcpzfQia4@`WB{C=?!hDl?B80mcs>L`;KjzxAHl{1YCmBtM+ zT!b+G_~q1_B4irs3I`qywA49q*v&ZaCoa)7AjntM5?8F~w$06PaQ()+*l zx;@GvH96nP14pvaGGoDC$Fcero9=hs=JPu-JBe`d8xM;LpqD@2)#?5m%9o;rkNdaj zx5f=3aVrjo+CNoNwu@+lF>m9PQO(Zw)ww+T>b+{^EF}p zv?3sS*9v?V0pFsGmk*T+hhB*O!-bL8c3xT7_;I$N^?FVQt0cf_^k2>X1* zkH0)_ifPT%kqC%G$$8XBr3P})2 zUNo?$;7(-6uM+{U0kEgDWA0M<9|YdES72X0RdaSf78Sd{Otu9t{Dm-v=%oL;v>|Du8$)A4r2`=UaT&pa!S5JLi(j+K{BvC%H?S_>{n-)p?{(xU??Nxp zMpEk&-@(QI;|kD#1>9h)=!%ek-JNuo0vxcc7-^)J{9k)TzhD8!9C+67zOwkoUV(qL zeG2ID09~2bQi_28>p~?1o?aYkMMcv8}D!`i1?U+jD=uI{f$T^+*Nu zJX~BFTmJjiqDNo?-zNocX7}%>NBSK-hgIl17omIlKes_@h6b!NtLKhO`S&^-;HjVx zu_y6&B zFJ%6p#iZ7u_}^&p2?2gH0gq3pC>u<`@1NfnXdw!q z)M83@Rp#I8XvqN-U04$RIUtNK2cbD!dICAMogy9|_w#9 zWC9m3Z~wsVH7joDb7MiH?fs2i+^=-WGe+}nkdtIM`&r}r^?YOz#N99FOZ1V<^i~iKJ zKYB7-18jn9YaOA}XCs)tspM>-t7IGf>AAfy`ua4oZ_cSRC#2E-zYjY`d~~G$Gg4n= za|Tw*DP4<#_Q@)7we|RSIdOR83cNd-+Lrthnr~+9)Aw3U1;;$)<3%JeHhBrALVU~p z;CFn>1>0J<$BB#;h=r%uH$kgSs5bnV12Ez}Q2)`&vPB2fGVK8bnwp2i?W>Dd+f>T0 z1wMMmx2yZru{|Czgc*?rz>}oFtwvD?0*1c#U(|5pn1Id5$&)y?bsF=AUgmU3{HHyj z&cN|RMaR9==(znLIK=*x%ymsE9;-2Wi#d=^%Y!BZ=`k*eZb7ny`1hBC_LBg(>BWJ1 z1plEHI?iBQ=J!1h0Zo@wj#trFs&J?Fy28MiP62pz6l~I6#;;pUFxQc)xJd*W6Nh{A z{+H;K8f41T!?&Mkc8?MV=hc{tF{ma;!G7+vRE(H{F;o6WGZd(Z?qK?f_u|Zix7zp1 z2>$Rq0ToNz=Mlq98bhDcMMp}{mQ|;nt5QYp)Iv7rHdVM^4g0~_fBlJTR@KOx?0_F`M~+bqI4>b2BTX znE)mwd~`@2+1yZI{9rF%FY(#EieD8+%g{%90hu1j4HS#@>1)AISzGxF#;KR+`mUw( zFaK19;hpe%!nb}kV3scopG!NJEUsXf%Z265J(NaJR;SM;f=_2_D?tNw7wOI~@W|1C4v5-{L*z?0et)vG@7Y zPd~7pUURKEXVs`tHAhkIyLPu>zgiI@A4MUYwnhCPZyNAzhR`^qrCv#k??;7op7vT% zS?jw^@PtL|T!Wh_&Ky#@{JoxZBbW`;%6nP0Ppcp$zWBN76ME(EQyu7YF@^``JBj#~ zhTX4#?VW3WbT@V>R2$yl^>OgR%K!SczrlhE)qI<^Ih^ZufHTebyFbyzm~=Gh5f^E( zGRd!0JmG?nh`=y6x?n&iI3rex!SQ3B{P<~*^@U7}c<~mORf1oa&`<5-*wGsCgn{S^a+#i`rWh#g2lt+RWwYuyDOSU7-`bo%x7IG{w}`P8zwb`KFgvk z7kW(L@Y0Yy(+TX(*T#oORIzOuQeAP{_NzwMyipl{2hOfnKP@vACYztu?0PL% zpY4OItYcqDGp1u5e^Ic)>z`~Wk%Gs!e|tOh>LJ>qm{pg{6+ECOx7lDzYH)qLtY52D zq*n{qilnB=ejK?y-&~O99v?v@;^)T%REHfAJ?Od81$8?~S`2E-`MCB(_-?I`?PuKT)cu zw8H=7Ome?x(W1NfM?{irDE;-Ug=lXG7OBHMJnDK}YX_|#_iuMF*n#gS64CI%S>~i} zA4^>4dQk*T6!KEOr#2BC;ADZaDsC|t8^^@^Yl+}|Da>KZ;{ef)4vA}niyxx}JDhy9 znXaVM?zKbK4A&X6+1d;mZ_dbCuUj%&eUOFgA?*$GQxApvVLh#`MgypK%I$LqyD!6S zKdf^kGk0pz@q)P%{q@v*8pD*z_OQf}xGY}vy22NlvpdahrEf?8nK|VNlFn0dZ58Cu zFyZ?C8ilw$39ZZtJCADtd;&0{D*FKTJ$NxW;TVszm{w zDefu%Gf(~KDyz!_UOzRs%g~L!HZ%y8e`lnJkW1k$8u#`r+MW)oJINHpNf-;SlPgrj z%&}|Xv6Mj;R+K>pkpW0vEmIL9ibVnKE8%8o7ork6?@xlG-n)25lvjHXg*;vA2-z33 zd?dQ|Q?-Zt3-=PZzpDdoJzlUT4SY7u#kSJB?G0{RLoUEk7YRrF8?cJ%@QhXQ+m}wc zLc5Iu;6@4wS*{~%KO3APiBbEXEpaC%19tSclfb`lU)DWB8Eg`c81rn!n`y-$6?x&TbfNtoyeEE( zzc2FqU_%Gp#UBW6#T)dGU!!To@MKA^g1@(xnhxgJ;0}TL*zmRz;`2Rn{EQ>wlHyOi z=&ekBbp(PMK20Qzx{HmtugeyZhxdoJ9m{IvGO-(;6WC9MZ&4kC=}0@h%&0sRr;)Bi zC2R%5A8?x-`t^Yo$U{dSa2fJma99_FFy4TK=5&MD1P0lzDH3tUHf%0!Lm2~u1=Gsz zi+502gi8Vu(zmEn9DPtX4FuNWU;g_gF9y7A$B0JUkt)ygM*_DPje~EXq^NpnrHh(( z+V#nb%BnIVpeEtL&M)KP9X&zSNC4)j=!fnFP|mP|65Rx$B$VAcPHRopv%jLOZP%Q= zqV;y)n6hh`zU7m@G@3g)%sij!jj~0P;zD9>7#~Hl(JLi(f(fUZCAVb)NKq(-+Yprm zS_Za`J&R8rBpMYFg>O5gP` zH|RcF%!po3%z~@M@aj^dZzVsMY@*0h`k2bk7tQNtZ^H0eD6Ug*fF#Vcgn8S_*b{Jr zroqQvutFGH)It>XhI{!IKlQ2j!u|K|pTnMxK~cj2*gC^>j%ih-xpbaX?Y!8;Ok!NQeNNPMwfYc0~-Osur{!-MW1eQY5aErFXONiMla`= zE=&+SWQ|#iC*A(WXiV(G=S$tjIgy`%VhWz{`=7X-ULdj#jF0MaL~s*EzuMSJ zD=3qTinO{wtSM8+#b``f3bGhbCr?6cg>yk@_iakP<&D8aQ^ns5`5;n}FP5kQdT?k~ z=vtz}pvyV7Mb#bW5GlPQJJU$VU=)BR{f z7}_UAK42q1aW;LJQ478xcUj&bb)^)Bj+%4+p>e5;ES5ABEK7TSU`BfaRhoEL(jHlp zu*{m0F+suW9CkZ0d?^Q5sKDOSKgpN<6EKwLetaJ2DUABh)gT1JwhRDlffOI?yweQ+U(l$%n&r`Q|B7)ygCoBzC-OU z>^a(6K+E5J%Kow|j}!+!Iui^YTl89dL5+^Moswv$yxX;H=Vehg<6ScOq|*Zji{tT` z-WIi0q(2>ubA!D^{7OpO`Y3V>CxRxVzb`(IM>EVHI=Lt>p4;3v!pc)>nhn`mlqOBT+_XYyVB|V3&Nn zNHAf*vR_|Xese4L;M2tWo}H)vh9d)MWs?`ZoAk9{U8yj^IgVJjnNJ|>WvBoN*UUq0 zKB!O1bZbs(npowaI#KE05R*|D#kGVVem@IyoVLo}4@uYZ*?2E0zU}%kXZ^i#wZ!G< zj8vudYxIg)kw;N`y;F9-{iEjiv}nhXpLpOAx?A7tRc9h&`6mv*pw&-S1c0VO+jTYf zFKBbFL@1-W#$mhQ;gbi;93~g$qp83Vbc~EjDW5A+i(UPSb&bB1ve<^Tsq`@mSgrG_ z2vf7v4GZYxR{9fYm(t)RmtzV`f5PN|W?yF{UX$3Y(NHH*9~sh{zqTTvrc+93+f#nf zLXK|$dncrzZ5uK92@@0Q5&g%8I{J~RuUCd(Bkk4g&m7481c-~Q*iQ4}9r-1vDy@TO_8BF$sjwIdq7gcN|$|}0*b>WZ7B1C}p8*N+iA{3GV2=aXH=WuD6ZIM(x zSgt55YZiF5-#Y(l=lN?Bi~b`$Zc~w0d+v<^-*K+H_nsM`3)}82HWx?*nt8vvzs$y; zTrBLshE$Sj=jw;XH&y%T;-R#?waxy9{c5_IP${6{q8mBO8-ZWgt$uQz@D|b3=EK(} z%+rqX#HUp$4>zSM6G2xL4{XkHLiZRmNW_;0U5U86z*oBD?Hh9B(2_Sq#i$pQVmF~|RA$pC5IywT z9xdYL^GFyC^rHv5_Zq|E!LZL!2bpfJh^KXQ?b8GJ191vPr0+$;4G;H zDWMs>w>j1*^(x-r`)UlJcLadXs+AJvGQsnCQ$f?T7;V!;uxgrEIfuJmwmcg-O9Uj`XK7RA$dN57unZtV>;9%x1FacTOi zcmqfFw6ERGi#ClwvfD76%^yl}xgSX7MY2NnQ1Z2^bUpXhw)CmhCNJUqw+EYEpMlun zE%q|{pYEx9mjF9ZjfljM8si1xs>Ow9S0kxp#v(mOF;g3$c%Sx@#Pt0O#-Y2MQIyF}JU@WwUVfIO zEMn3~NYz#Wu_F{u0q&bx%zs;+9C3x?-dajUy;l6QSHE=WMV}F~7b0Gh#4-M)y9HEC zwOoSSx$9DM6?;9Q)mR7%NO2T?kFs7?6*@bhZJAHtUW|NQ zZi%Rg<&_dML;%Q>1@Gt}Vd3IiM!X6QEZ3>cp;S@X4H{v&v8ktq3t=+dYM|7j<;D=kMr73LYZowX3fpy zpZ7^rW4~ZB{mj0$bHVY{j#Y1KS@(toV8$k9xki@wpP3sAm1ec8N5;JEiKMeYMkHRB zro{$aO+QPz5~Ba8ZXSEK<5yIbCR{Dos*k^xO!Y9j4fQn`El@0U@1FD(_#myu^@ zncXTb-yG66K@!PS=_e=necG+ifdbsLcO@im+I*6n^;&9rTb_g(L{qw5{way(dsjD( zX>+=5Pbx4;>ZX2+FDS6B{w9ovU>JOa5YQ{<+A~iUnm~;T+`<8FZ1gjr5(;b;9HzUT z4o30t?0!8#mat$__8ee~DE()PpkGX-s>HE-qh&KmJc{a|2N;a#o+zsR%9z}hjuvvB z#)xL`UBGz7@yqv1hwg=UY*;PDN@10)Uk~Pl=j$Z3C&617Ix@{wN!Wh~K`> zO>~uO<7b!nk=az_`LFWfsQw~9n-&fTy%hE;NRZP8Pc#?lTL=oK7H{PGEbccWS?{<_ z@pm*u{Ea;;;ue!`obPXFzZb5T=&LshDMl!hT)&LbQ>(pVHpDeGXfvMCAIhBO*we(4 zM7<_pu%FBVF9dF>!`tiPFqSO!aj%r-K)>$*@_R=sZ3;})Car1*r2T!8%ITS%Ug3Ek zM)-_Gt?bhR-Wvmyj2d7&7jWAyU+DYK@T&C-kOA&gYYgg9Mok~EX_%Lf;<~uWxQI(> z3jjP9T|mkJYmiMo99J5cw7Q)I4-p^)_g;3QPkVMd)mV=2#K8L+XF`}=U~BP2;dlbm1@n2ymM0D=cKJrqcWZQ;zD6pWYb(s=JQ7}**=N&`h$OGtXa{t^l$jz z&Lglthn$+_$kBz5O_W{oLTB+9AooK0NppiG6xw3H>Ynl5#DAuDG_Qxk_J#IJDAu-4 z#aj^Jjjfge7UHoG`Q!nYTt6aryGj30=6LNc;Ji>mC8yj;i>2bos7F6#$G} zY2+Xd+*}kQp?gN}*RnoEhwL%$yVUrzQh3=`RkJ{}6*|FNUUOz59;^WIbs+IV^7aV9 zl>WD}d799aEkK(oR@F`r6!k_x1CkGbT@TP+s{o?}(3U2XNC)85jHmhAMHk16oABoC zPu%F&e*V#?AJuGjZ5dm>r_rX5j{)>W(hUQ%4q!57gnZLmqvcoa`06CocUApz`vDuq zGIIX91q4*--;c}FHf?MhS?qm0>VsOF8jzCBED3qA#JwQ-#s7L6XQsyEy@smkj~D=A z)cKAWTwU6h>1y}cpwq*O0@J${WSVJD9E|yZnfC2S1_D6LCNK_{ct2?z5lLWkro4nN34%4L5`Ksu{V$_fn zY?fSfO_eYv(;A!gv}wH%u(eB&Zrx0hlZIEL07N#t2eex=|8nv9f1W)pfJdwOZW|2=yE&0}#_`rK4MA z*`37Y_2*Hg^{vdd^-u^$WZQ+;o8C@5VKW52f3ZiOKz16ivzq&Oi|w&)UcZ4X`B7O0 z=p{&&nUpWp6MOgK0JrBuL1^dchp!tg!hZ1C`_O}KADQ8l+dWJ>2%|F#UonQW?uSa6 z-unFjPp5HYx~M4!{qN*E!{WofSvElJ!yD<0x&M;j4di|dWHKL>e@VCUAUI)B>A7OtGq!(x;YJ$x4*Q%Y6P&cfP8V+-6-kXW5@p#Vq9bZ z0h~YVhls>K|6agNI$OM&HY)I+hnICTyf@~$f3sWvO1XN)0QWTt=_|Z_n*JJI z{`oQ9q5;>Ly<|He`RCu;1Qx*}r%>YUq26EqH&7OMTwnUm)x3YcZ5HLt@C@Ue!~glV zZ)tCa_qOQZ%RgWD5~yrB{T2IG?4Kv&$%mIf_$2ep*CFMfX9ICl`*5K2gYxz)=3mcy z7yrSsH_=~f=k_n}eb9J}NHS?8U!J(t{|!?l+fM(OP3!l{SpF)la`bx@Aiazn8zi9f z++J*H+UKVb94UCxEq*fZ`sDFrrq{Kkcz^Q%KrrEd;pMWkcv((l=KtgaFWTPn&m^Cd z9qF0l2Gb|yPrUY5KF;Iyph%WO_vB*fYxYWEY#O1s!MjT0J_2%{$|3L`j38Nyj=l1+ZA*=zqh~t|K^f}{U z5jrrAiuqQ*xnrK6UZ&0VF6@7kl>ckt$H>wYige5TdFG=2*WYgcVv-7M&DxO)n%kqs zzb5Gw6|lJIq6F%0&87cUBK7$4#&TTFLVOheac@3Gze%W|lc$IOm+Ne9LU^*Le6jyf zAqMR{4?KP}&wutF?=fE9l=H}j)MxzTjcFKfs)!it#G=yw`MTK~A3e+e z)bpQ9>}Vd`tUb(M*<*}VmVQPKuvR*to%1KB{_;QYRVbH&+w?l9=RKOKrM!^f`EPpat@2l_#5`23}%4R`^rTdqH~aeeGanL zTw^;Kr&qJlcLJ(|rZw=ZB8;>d3}Ux-~1(RT)vxOHOH@q^0aJ%x6p4?dG*n zIXC=d1`aVh`UzW`E|W5C=E{&Wn$73Uh00}~VD$ne-Z^q(!rw{-0O^T%prW~I-qy2f;y_N0=b?oCwJ%& zb*)b^%)&yf-U(1EOBiISV%HTmCgMuo3HDj@Ps%z=!@tXnK2zWUpg*MZ+}?<-s`YeUhj4Q$(va}w z>a*Z*h1O=2l0IYF42Aer_#=vo!1=ZSu4#|*->Bjlcy?468`g{Ohi71%Lfi9nSIeg7 zDhRNzyIA8aXUEvK_i7=>=A=bkCPO8(LD)(?|JS=+ITC@LtNZ$O@dUw^HG|$}S>9EX zqXnj}W?G)O60(liD<_)^9MpK^?@FT_2zG6%J&uSL#DHV{AGPowWS^x;(}&0r78QD;dZ@HS{gV?;u$RPRqhha~x=UrCY7IQk>( z+sW}=YP9mp!ITPBwY*1!AG;Wt`%fB8xxymLqXL=rm&|yF}s>gKCWR7_-J-@?;AIT zUVBBTz)X#|9&-0+ea)t++9#vI2CvC|X3Og1rb}t$3~`e5ls_djKdqhKpQ5tBEdgV<DVvGsZ&zQ+r9N+&2ZWgKZ^K4tUy)$Kw#N+@i}j=&-?o1h zo7Km&+-G%sk%>!g`zdJ&U)(v1zW+D10nmakQj_hu^~fn81h5i(C7NJ`x{mJ(njUS^ z4q(yGYc6d&Z%P*dcT6Sw9#S`#JR&>&*xu#e)$NE{Gr9-kFn(d>bF|3>hqjdpM`!dN zCaYVF<++1pmUyg+pB!}tUl`}B$k%OCQsUB7LWS(+d#o&n_2{ifbK-!r+R&65HeX-I zV2`NIQyLJ=T&=6j0uVJ4Q*pTmbg3!&K?O(xeTGkH1X0rJm7kcDVfhICS&I~ zX1)Q9^4=q|QEe_k)+$?Kfn)NFtscmwhP3`dX;DaGg=d-n?7gdrDSh+N6&|pVU22sj zJ2%#yixZf@nB)gG(`Lied}o7^B|*S!Y^7Ok&vq#rdYtRhHl;P09sh)n)EI{YdI2J- z&DAQ;?k_L@{9IOdxHcj+v2>xMQfAbk>zT7;+UPW5&@yl;_-D*{wrK_ykQw9ISS>&V zy7(iRz@VTdsTuQBQkh8k%%2o7Eu9^aDHJ7c)jYn4TMC9JBpFZi04z9O zJ_hBi=ZL|^xX)Pi1PQrrt$F&_F|*~o8eYF{LbHlFqI8r1)G@zY>G40WA+7obgPSYtCefy=@DHOFJ25cdNx4PD(F zeC4EXqp_MnOTyCFYqZjz`acr%z&n2ia0sC@jj)7YSPiA&)($NM`Jx{5A0ek!E>Hn( zL|w=0tn7THQ36GeTWAsiaZ7Du;-RCn!t^+Rz)Jk|1y%lF*?9*V&r&63RWeL#wgWj| zKD2ga(Ci-%8Zw@T#!Ss1lix5JHoCFNi7s2t$MvP|{wUr$ibL*aS&!B3aX(*uZO&QwUuzzbc$n?2(s??P%_z4h zf|@{we}|BuVm8&5RHGjc3DeL-LdeC{L^0Z2zU~#csUwqTdqccbz7(yoW=<$ndEx zD`HL*+*&e4H(&>xXY4B&>8f^`Uy}t&KU5M}BM8}5W$_44mF^Fw$BiVHAB0mjiXo1M z4lcz>Xva1%z_GNVDKktFR8-~*z2nUp z`_(FCxAl1``6Sl-mUAJ#625;W5sHDij-XXb@seP zAxrLSGIR`?WF&W+2%r2I;2!W7_LVJP1y}~tvyv>V0e@Ds?za#(FCXf;CP3H zyoTtm-Qy}f*ZvD8j zIUyEEU9Hv+rEo10Sh=M7cfH(3T%5>2qTNQ1E7(CL(Cv!B|9W)3;|4zsgIqhR?j!DU zU<)kfx~jNGaoA>z^?s5iKR^ULBNr@lF967Vpn(0hj7;0JXyut<%kS8EvOuIzlAWZ7 zhx=y}(BmEXJnLzkN?x12cJs?}y{vZ8Kl7(wHwIRm8Y`brC?Dm}QiEORa4C2sJT3M55j7Vz4&~QuXy+K_lb-kP}k+iE7 zEedschKujjFH;HrRcF@pDt_kdDCNcUqA5s*utOy*!WmTX?8X~pSS99hGj07FL?&Zx z@Z-O^6t3?U+Z5M0YJm&b??h3FCX(_oLG}A%G!xl}*&=pq1gPhA6HMo#94>_kyFuv& z9+p*vvP^Pg^wsL)7RlsE+r2YgODCT!=IMD-y1b%Ig1ZtBCqbAGkjhY?MS>9aZziQQ z&Y7wf8ks1n#FO)5?h`!9_#2H+$_!xfXi$D37;p#9`X`kdf;YU~v=-`}4ezhS6~vf3 zA3i_x{UtTw`D0-C*6P~8FA63d&G3G7MR}C(?5mjzc$f)|9={_U4mrO%EwX4dOS8)a z8G*TLQsFSDB|2BWr_b4vra~dEthE4 zA}*rQ?K<41f0$9LbN1cESE0WZlKNC8>|M-7F*70~C%)48;` z7Q;*@3U+{Fzin2J$mG$TK%ujF3R5WJBJ=c(Uqx4x~jC1?nuNk zx^A6+!hrf)<1{JPRvo)q&^}A}ULN-;px`!IS+jgelS^hEI3($1@bYEy3+J~*NQ{j2 z;5;C?fQ3uK$d%+J?XQ(}ZkqXsH~AG7$O@o%>CM9pOfw6^a`ElHUr!<4X#*>?!3YdhVIei-nZvZ01irn*;oz~Q}2GB$8W9I z1HW5rgvvcW6tmpS-(NU^H4X%aqMrrO15IKQ(Xo#E+B^J$;m|KURqc#{8b!($luNi{ zimQ`kqoXv_R^ z`TBKRG*IsEwmtQYd-27A{l&e4$VB;cdD+rfUAy>WgO$^as{M)TmVKS{5RW)E(~I&b z8HJcmY4-#IrU*5oWD1brn>^35tH$i3abu>H9_qF!JXJ53m-zyaJx5Q6W$p)q^-jO# zT7ih7G4AQNNSVSI1MdSOCv3mBCptQ}p7d%L$0k`a7GIlE2cW;ev}6L%kL&D^>i4An zr#J1~kzKNeRUQ^*6haMg9fZO^rCa`flWi8x3AP3>*TCa=*vA*?*lYSc5=bUo|lf>OXfXorE`=O8o2x&V4M}yy80`Vo_5R z(=wWURtu}~$v>WrC@%*>hJ^deV%k(KW7gyokC4Q04p$nQ*7wGF`Atax>zxXespWMG5o*)RAuHrpmK0WM~ zZu4p+jJ%^EbLMTiBd@-$R`mfgoOMrqVrF@zO2} zY8Mvq&8lq3<>Hhly+_~Ujal&H-zw*Jk+pJV1~soh1Kg9k0XXDhi4!HpN^>&B$dWM+ z`;19p2(VgQHmnFJveIdd0eJRZEthEe{bS0bKR*L<0R<*_&>7GK>f_g8G-c&zAP=2g zv^BczooD*u@<8j~v5502{z%yv;_|K<&;_;SBPn0LO%r|A5bf-qOmH|-jHKADw#_Wr zG&3sl#_eQd+UKc7Jd~@j@wQ9EZJ53q4CpP*?Erm{;w1gjrAs32){E`_l6Z=ZNr?*i zxPL;=%9J6E`ng-YKeletvgLe$1Q+Vkv*$4rTcZ81#UE*+eP%zfU$*+6T;fLIF(aSf zez@~@8^+^O=b6}}vpf1MJHy%u#P1O1N6O1X1S0(;Z{57A-u+_k4Q{9r>pH(E(Fb_A zs#$Qtg3BbhWURr3wisz{FCyaQl=lLuayZ5Eqs8s*JTKn~f z&CFtYjJw`H@KJ8L=a^#T4|)0cXWQu?1+*UQ1s@FWh#qkD*q_j%nx#b>sUU)o3;A+? z)V{R!Sn2mS30E~>C1(bRpvaN$$wD2k<$S)SE}h~Ikhsubl5r1T4&FW7Px!n)Wh|Mc zR%Jr3blNF|^eqUhqc9WtQMdMMw$VPWq8ILHo>a9RG9riAm@nt`Z*x2`iD7Cg`ql4P z@E9uSxEKvA%?+D4CY5g@T9~+KBw~N)MC-d6TTG>7L)}@C!PjhVA`?4^Ls?no` zj|%&jdnBhc+{#^F%b5DE`da6Bw7E<&?r-q_*sE%?k3FwzK?sS_n>IT~nzq0Btc63d zUGXBHc*yvBHmSt#$UvIc`}7bQsz-YL34C?M1~uc%SpoP9=x@Y;ToIesbFw}!Emac7 z$4j?mI#03gcA>WZ&HFToFw?zdy|;yT6Q5DB8&tk4d1Aqoi22Y8y8raE;VNJAuu7(w z^m7NJBKseUm2k7*-9|Cb$FI7fS~T=6GUFjc4keBNVkeO>Ha9nu_)7 zzExXKym<(lt8x7E3lFV_3edv(MF9jc*;QMkL*AAa_UXr3hOg(y+{0^c=sMDKV*E?t zutA@mNJsO{)1~gbL&u|ynw71{g~RF_Ke9E|arX^LS?a`MTb3yN>R0^Pi`&xsUFa+y$XKx;dzF;G}CJ>#B9xRCEF;kg}099pXVao5r$-HueTvw}g z7qMZ1%JtOo;V7UDX0P|t#^k@r&K6_*#<%FplF*mAkt9zdM1mE&Qv2s;0)yHCS*-`n zUz+5B@HGMG5RdFQ-6dOQCC#!8VYDC5NgR4xmsWIEQO1`)JoTqXzs^AxS*wIuOX$hM z-s$BP3g~|aMy`aP{VuUY;DOBh$DQ-AJU!<+T)N(G$bWzwg{gW2j8wmOClR}gY?nA? zev7j{XDOP8DjaKyXo5MmHqrO*@my5ySRpo7eSyGGu$U2YFsO-$od}!B z)%UUG5lt6V@qhZfv8?f4P?i+Ur~fT%3|IP%7G^K$U)3Gv1{GPEiq&bs}z7OzWpHP$Y4@P;=rz5Sll^E0@|eB zBljhYM&u zb>|2d!j14X$(B({Z1|D%-}cWtYAHq97dGoCausnt%QD!uxAT^rmx%V9%l0-AuS z&u&TefhJM|?F&%88+L)}6w^Q}*f4D{U{%LV@w?}0yvgP|%S?@(pW;kLZn^aSR)Ocf z<@($ti6h$B#+6`H_Gi@0*R9rYR%p0*%j9My|BF6y-dxle=I#fhPSAaC@5Mt@In}ZD#-;`zcK`> zE2T`t04qkAON(;YT7x7CbC#S$cno~Jk;NINAZ&bfftf&z8foO4+(^IAR%HR@G#d{l z3Io$gO0=CMWolY&d2J2$=?od)mih|(qGYnQ!7+Qx?fc^~9F7V%;eNmzwL%wf<)Xc) zR-AmWI>?nRqdXe4mr?^OFXhc|a@&5US=0=t8L!nJDn6WfMLQA0Z9Dl=E{*x)6NGp? zpgynGx&3))hGMmg79#kdlTIl+Y%td$Jq$cDBJQtE6@(`0MdEF6Np^2MjwwnZD#H;A zTIs)~omT)Wqem-jquam{7o5|U;Xn1KhEi2wKP)F@~+3&I>)2KTDtN*zZzqaPj_X z_Snkw;>$=JgUYsVrTv^`a`C+s9rb@1>x&|shy4ly`+9GD{LYZNKJc{@$svQzra+)Z zuKTjw7&xU=*A*UQKV;|@3;b!Os*PrAwzULQQ*!KKhECW`PJb)B-=c4G`i~aCmD2Am z<%L{uO^OSUlCWOLtAGZli`kcw$!6*r2xXTOeL0??=!<9h?9t3?bI6Tkb)9|OqxW+O z0QZq(Jd*oxwt_kVn}PDj_N~BLPR07lxH&5V6il-9pnKRQ0r3#~clG>)L1Tw#p!G)e zc2-8IUT&D%`b8;JEPBoEhDOIk&mr=v;v9GUpG=B(lX1RzpBnGi>t7~A&x;3muxXtC zB7ZBA(ye;0bO@P9@zZ*2998*}vcQKa@i)F4%O0~p9 zwcVO(;t6gCoGHeV1kgnxUt}EPr1GK4bUMsi`|5lrh2L?eog@DJ`#ZW#;36Yfs~KN| z)6&cGW}w+abe8mID9~FRzHEwq1a5*CC-g*+Yg-QKd=)9opY(`tyvHKhs@5FPb@wh+g6Mm@E{AmnpmWgUI z@B;~SGX4*14TI~ddQMjd^{z8p&fmvn^-{gKl2sUzyDO=Rf43_ce)!}4x(4CX7#blK zv{={I81$cLgo58=yDTQF>^Md;f2F~eSB^Ek-&B6>FW@%D2jXi2fvP}N=g9MU#r(q3 ztK;`>058l~AAlGFpku>oU;p}p!B6uQQrh1&zJHp+jyONws;C4D9_uq^YgryUKF>WE zhaFZjTtBB1^S}CeZN5_QjpZrN9ABkDdHX5V!g(KM-PCh?Vc%1eU$vgwf8X}XCo5ZRgds#D+}Oc_+_4$te{JGB z&B^oh+50o)!xz&dy8VYX@ebX=C> z@gR4YPshZU*)Pe}vduJuviW=wq&2^CYOYjw9&y2d^$FezRSmG}dtdx?sTgV)pqE)S zZNXP)P@jpM2SZ`Gj+q^OXM|e^qmJi0_^|dwD4pz&%Q%cN~KAPI3H>liiz_0hxZZ*~X;XnrfhhvX}UEXk(U zz&!#c#f1JOBSyzEJx)GL<%*(*r}kEgt&2e5 zEe5}eE?95}d2!6cd%AQaYb!$}#^2vsBp+_Lwat7<>o?>ZnI0zn9JnVB_UW>PH)54K zt%o@tLm6!0JptE5tq)34f-t+0fty%e2gC!hK-WqJP;I)B-T86JzCn26(6LG2p2so2 z-!#bUop_n}PK`s8A^3zWe)E}DIab|}WU}P{o)?KbfnNxiwM&E6Gk=AhD?C!K9gyZV zJ416dvM-n&bXE#H9NQ1xY)2s&%?>KC-Sgd(aH@+@+pP7?LOmI<>wK;=*wm;JG|qte=ebFi92qeHOT+}K7Uj*`$sGBg_3K-ykY4-L8Pf%tP^TGB zYw<9*#3Z^UYFm`E&96pj=m*oi88*1mb$KPD^)Wr=P}5=@UJ6?9y;gHx=vt?3T#t{1 zi{oN(jR5<}n9+Gtlv<^^buyoAh3-UMfTwPiWz8COMw1PUO|wWV?qgZ&pGCv%i=Kti z5?ku3QqyIy%cjnn*Y;iPcPJg$ABG&7#wbyUyU*D}gnb(x@H#&&(y&NPqnp+(< zWCdDDM+?{n#I{~GdfP*LLkF$~jjWHFT6n>+Lh!&~a9>nWn~KMoBd1@_;aEes($Jbq z9~oQ^AK0YV^FZooesc>XpIh2|INGm;4+u(GS>;nAVw)nEZtqx~7YSI&_lZcTZ8EK*yW{{LsCve^ zW}RdGu> zIYiFb=dHnD8qtGF3ur&Y`XNqdrhc7lnGsk2$5R5!hL@oV=7$9z)e1~Zk*4pmp1!+& zuUkgi%ov$zvwD4*skwhP{?XDXoPqSb-16yp8^B-SRI<09*k$$5QLR1$eL63eFTAmp^mWj)y(nB-mnpYO_3&fH5WKAU<_23FU@CL%FD z83B(L<}!_)5F1}*LdZntNh`IgMn$d-^55f0UQi0=;%YR(WT7^@=U*!5ikH**G-pVI z+)PJpmlY){hvsIS-HfO$A(=1wn;~DPwkPh7<$CQsY3yU;zA7xT`D7`HJ1%Z+MLF`@ zhtm01>*_?&RD2AbNY}ub<%J+tUwf3Qf0ZzW15ip}ff%ARqN5Huo71m6wP!JjVE(E* zfrYouCj2KzdA2sGBuRi(v(PTluS&|oO=t{ToVo*T+1=B}%k+}cZF_X+=^bM|Mo6k% zP^iFq7$dBl31n86=v@JI1?qjMEyv8F#GV2CF^PRAC5a;z=-lTH3EO2_1qF$tag)wz zZ2+72)oJ=8ig}Q3+)jOV?cv(waPjr#fy!V&z~PP%B}yu8Y)#>noSfX`?h2*(*bZ^! zj4icK4OIxO z?)vHkz35sb=@G?l>@=O{8RJ*%Tw@J9uN~17x%^1cfrj+%iFyc zM90Nc#KU=u)HaOeY+2@O|~CLHXLhYh^XnK|qFF+%(?T?s8^IPKJd_)F#L~ zG7q}lep<9QmE@zN$s@J^$cXzoKa}0ks{_Ec$8%Ca6km*aoSp4t*{U@^A4(b+b3!isJ;ZsnKf{~OQL5a)W91*#!yK?D0he_<{HMx)-!^U+-%7xzKi zbiW!tAKDg7qxpr3H0mkNj_dC>1 zQYMmuH+gvL`I=+4?&ti$V07(@N0~}U$4Oj_sW4oUadoY-s}5^dy#OD*-9;bj^0TeR z7j2mCLY|x>Ix|n3;%fk4|3Iyvnq+9@qMmo;yZebocBFWKu=p;cA{X{}!8C_u+S zj7{XmZy#bW78oI|w%jWpAC#sb6bOK0;JuYQ7(kS?K z=fnEWjux*a`{S5~z}zA_hf4TOLY=!e-|<(LkMKE9TeKABY~%;5l54s=lz?G7USLwf_Iud+V?!yZ-@L39$%8M37QS zK|(^L28x1$fP!>NBPE?1OjJ~m6p(HZi76=!DoA%oPP&=oU}NJw)5qe`-}8O{djEN^ zi_2@sy>p-Q={_n4q1PtkJ68ttn=EBl@lZca#!qP#u?4o|7raq8*9E{aexZ8vq}|)vMjzoJi8I;A&qtI=Aqj8V+pZgyw_WvbH@ZtSzTTdb-Em;4GAr;H z?}EE@&b*V*^YE;ZHIEvFzQHgG>_2CGdF<(ERueHvF2-~z7_n^Dr<#XM|$gVwPVXDou zi;ZvVv+quQ7UEf_P%phVrkmqV zQR+OAkdNe@4?q78Q=t25^H>w@{BKvidh|g@%WCb^-MbtuvIrSHgdR4(&)ZE^H3Ir? z#fUwi>t*krsre?<8n;T>pYQ4_y@&i=YdE)gb@jc)k1tgw17RDrd?&Zd)t>sS2r8h&(Gk zlYovE&JF32f}%_;6*5m@#QQol&`ZNH)_q)RytKI0Quij={vsQzyv6KYK?UHvF0d!! z#-pFd!G_*k6M3W-R8lN2heNbvwob=HdS`U(8!gy&b>mx4Z^e**v#QI|Od-#w z#d=<;MY7HJ`{N~(yRzvHRS=EmH##yBjd!gm7#aLaV}IT97w*p|PwWJVFNH1*WA?QK zql! zS-=T;8o3+QBK(jnf=q@ONzrl1?$R$`e`Ue$-`rPcl^$P)%4My>73o)Q&*d)=*F?!a z^szJCCfJL$Id_sl9)y-ZMg6vr#6wxC6A46AF-QIdZ4l9UY_u0R30cg#S9CmMf$i_e*b z>M7Wx*BR$4#&ksEDa)_gIsOP>qZ^;=PTGcO7cK=W#$2c`Lsd{4=VX*PhB<%cxs$}5rl7M=QbQkIhkpY!Ob$LAui1Tss3wie}LI=8` z1@X6E-M$LJt+8t5Z)8~9)h->lZ0J?Gc_!dD?EDfW^pURH58!ydm4WmRulk0W3#9u4 zf?DQfv;GKb!lv?~_Q03>7w$Wj{u#DFl7bn2{18Rmh5utGQ-qy}E-}#jeJ3PfC(q`u zaQ}TLC4`-9lU2|DK~sO*90?8B$r;L%e_ey>F~weh#&NOsgMUOLkdE+~V_*?6#G#zs z#?ntpsaEi*cH^Ts{x4zr%M+n zzSbalDbcM=(15!!J7C$~-k#tVBOLGS{R7^stE4Nv+$TZL6ka6nIQ-19`bd$4STQKq zK^@t!yp-w^$LR?tE?7`B`v}ETDVK$p*#AUbh^oF@l~w#|&O7}LVu1aVh&rPzx{)(VJ7Yps&3nNKj^4t(|}NPYW1 zlVIuBiEcKR%}DFa+h-k5{GA@|A0#>SwC_Gi9oeKY>%WYZAAB7`d+qEQeI02-uL%UP z+>Z_K#_eMz`1AW?g*||(rVCTl{<^{bD=fm7mgO%QdR^GMbT9g^&*>9o9(dYE1maL{ z{uPJ%DS)Kw4y0ZHySrlORi{TA_~7SXe*Z+pNR&zbw6FK*31-H=0QDb-+x>bILBc8g zkDcWL_j^VVQkp;r=l4@S==!7BCWxyNY57ab13|f69Vv3nBJkx$fAC>{Kuti2B`wj- z)qLj>c~!I$lxSRV33HF_Z#aTC)7(Rcf!z4*Uc^pq)+PF5207Z!`K=?ssG&V_X!quS zp6ZG^3(uux7xT)ZGf?>QRd(0-i=y^pZYIis1AmuP*D|EF?~kqD1TAhNDTw2I`nrJ( zkaNq+%d^(-47b2B_C~gcY3V|@RdT&ZMPb`7KvyFNdD^GHO4vQ{0Gj>6XCS=1N+4`^ zuFPd=e`d|YgEXPEU0#Sw!)`Uu%8iAsqw({Z}@@VpeAhhLb==Wn8RBAEpDCd zWv4i~)>zTWF^9>?Pj6D#qOM1>J^U-Ry_9T>@9T6@(-JT>< zTK`u*_K{9M@K$^kw6HIUjAL6wkY)oJHFNWQJE7S*15pXjos=k;E(TlcoZ&ur>V!b&?buh1j1b9)osh` zd6iu;qMX@Ww;%m3R(@cBC3unKDUce#P%b7K!;#f;KX#d!hvDy!39EX&iy{$Rv%0u^ zkcRghyZrM4OowuNB$JZn++X$mwS4KO7a) zS9I^3iHBi%{FOIGwO{FS&Tssw3fW)j^3g#HIT)bf$(ayVj(krxirlDif1R3Ta9#Q7 zi;$z$-B12@Fsk$Lw{I_Y1;syqZX&BI={R-cN+1mleA&ay-hEa67I%{pEX&a#e}sQ` z%J?U2s9K2Z?cG5s$VE{)WY!P#%gg^^N$Ejw^ydJB4TOiyr84w;9f8Y;F`)H&tcgbDp`HpD)@hF!G>tMH7@MK2NF>m z()aUTvdYnVaytp!8ocxEq5_yN43vL=bs|wT<4D-VWB?nR$&KX5mxlr|9}1e5B&enNH6&UKLkun z2o(hUKb7}*m&lRsJ$lh~DK=Kp^Ma}}7l@K4qen3}TSsPy7E135K%Lss-2!)}fhxXJ=VQr8jSNSu8lWv@FnJ}#Kbh!OBe zdiU@us&;&Ao;x$%#_GoHS`)O z-%8b79#iM{q5vk?1JO}I^ZieQX2LTZe%}xL<FVa`?KAPVJX4B<|SD5RWAzs(&m1q~tt1s+2^a5L=T{A7Wx-JF7rh*a! zB~oF69bA+WQ2ru54yG8({Wpd7^QDqR^b85&8oVI8pAakV%9~9N?1@c@UKzYUAFtd^ zFHJtq@LhVrgl4b!MbPW67kY2%MyYZZuy?xd7!KS2ZJX>MCCs+J(L?&h8xO7!+;W1X;FKf^G{r==n z)8Lm+(w|6$ZVlzeiL;$QfBu}axxC@_Raygr{{g@j^*WX${HK5O?NhKC;e~+L>*I&Z zWW;JE+*j&rCmRLt>Rj_U_E+2LCmtU^_u+Ud?y&j#2{+}(^8)Pr_@msa!!UuKhvS%A zyKE`x3sh(HUKLm)D|u1{TN0$IX6kMHIz#Lssl=-L{|A8zXCWdzbUgHdUiK z(@T>54~skw2H~_L_tUg@^G33oPxYong+Rff{{|#rJ>bEGr!f2uuRTQPxj=*>K>uR& zv;U?ws+-`2K0?+%5BTj9i`798H55j0n?L-|MNG`WM)FsCriXS}VtHwVsNqVUW9Lss z`pYNjh~psgr(mG3M6v7S@Cg7{PLVt0yN~(rm87i?0Xz=&1@)TWfqQMOG|5mx&LLsw z!i7(7Bo8H=&G^k{q2mEylJ$3#*o}mr#Pj_V{2Ex;?ttt3fE;f{fkSkT_YO2HAc3E& zW%8X`;w-o|D;<;y^zax|UV&__rQB1C%Z~TwE!D)VGaB`R3YTjb%9#-BgaG|fTh%;c z&L`U?>y%qdSj1$EU1g|hh7!jhd$h(xbCl|cF2)WN^afd)A@ReeEzv!hwhlnBnRYi( zvuP~p9%W|c1JhgNowZ!9d^cmk9^u)1!BOyK4fP6neYYfqYxTw-pMh95j55$4S+paAKta{QU@>RyswvrjfGsZ{AiuZA08_`+kWeKZ z?M>#5D^!jywtg|?Fv`V$XJ{Uzm?zUqi#2#6K^NJBtT~hQ+d0~bbpaa^`<1UdiqY<~^g{cZMZT@tm)j?h`O+F%`KmT~l zaA;_2LrTzaq{@+^Uv-J}g`@S&)tjZx3Bl?-1AeKJPHx^?8-s|;TqkEQ+vvCk`!U0s z8DxoH=oHy9%*x1S?&|6xbkgWi~ofbxOX(VUqp)`AhmU-LhaxRs3AU(Gv-GxqhVsP*(-W zAK#>8=(VW`wE`(NF*$`V6-wuRg~K1XIamvDn9d4_M@8tCIC$v!f*DnfpU#KHIX_E) zzKbe02lE@wg2@(dXa%=ps@+@-y+Vboy+-NvZf~3}nN8Q3tQ4cvdLJ0+klUie9nrWT z+7&x#k#P1_`2z+CC;pZeu#py%W4}lk7M%koW`-x*v0G*fVa+P!I&g9cI(iQi1l*gAS_qgO*n|c6L(kvBDguSpk)j71N{4-k0&8 z^0bsRk?`~ak~#yJmfBhU^0x$z1$LD|A{TKkLVNu(mB;0GbeZT088ak7*Y*e_z}yj~ z#;KN=?aDL3B6j*6WF2C&F!=ow2ams{I&s8WN;PDC)Hdu|sDw&!U`4}L?}nl$H1|1| zT3z-kBauErBYw!e`NE(W$OSkwwG=mW7|WTi&!OT#5ejHTs%NL+l7DzGvL4)oeP#Z} zwQH|M#E~8gZzb%_wgOD2?3jmNeryWwS~aSh?aE+ZwoX1fZmYB1u<28k^6sI3Jx!zr zjZI&h6ym~U*U=Mp3CblaG}(9hMRcYMJ~AMYe)@71@nYoCM}zqOZvZR15mk2F_I;cfh5jdtco=DywuI+hYj|JGv&U znf?D=r~V6#0&2>;40O(*J;l&bUlD5hz( zi#?Q>yguJ)aD)U8j7(Rbv{)tL&V>9fId?-eTjxv_@TT{x zoNGN%Rjdd_CX{u)`75<7-jp27LZ-||^*rbKgpFjRoofuJ2(hY>V}j`p6u?HhlWJyx zAZ^b?wWOgm;Uink4xn+<^uw1ot)g*x_cs$fFh|S{$PH10p251Fi^yyiC)AOBYiT|q zDSi#M^H=wKdboeMn0ae>)zG7DWVo|bk=iqCHHWfRl7g z1X_%Fc&7|i6>_V0%ObJT(}7GbTMrwStyf%kFv8;Vwk@drIc3M|{a){Z`>!hJ@nv{6 z)d}xDJ?GS``4@U6?deunx@lZ&?(m1CQy|JA`B@~qb`bas%T^H{aW%MbUKW5)pB$SyH1)&bQ#tiV-V?2@2)i?uLTz7|DnrY27lSSM0q`v`T@54Ey8diUH#fZ;5V_#dh;sv5=&<+#< z+JZ!}Gw$J}pmD7ALL8`N`#npXtj^L)JdXNS{-2Oo3A#}V@B3!BFOFAc^q08? zIV<1p9e|BqbwIZ!;8DhOf-Vm{)!MfV;mb={v;jh3{C6RobB!JzobU44?o){pMcU97vO@f%qBXh*%#5g+%x6 zht;GLCcq44jT|gGH2AC3{;3&%e5zLgI-l%qY$kaCAyM7@qNqB&;oS8i)F)n@ouSiU z)>+&!o>%9Tz2(wddHW9+ApET6#w)E8NEmG2@^Ax|HU_J_2)i!@^ z>73h+yQ|u(ynfH(v)7q2bv|Y(d0xuTD708|KYPiyVqS79xT1l3b4Wk41`CSpj^bdJ zA6W7SaMijR3}Q~wy|_Ge-YkcQG(P&R+T-pP2s-@QHb~bPH|A*Kb0=CC-R~IHyzA$9 zAJ=~m|2|F)_O+EwRC!8#m|Xudy?LhnU33?ypmr(e{U7^xEOA~ zjQpt6mp!#LoQ+)Aut~0=i|$eOW_FPUa|U*TDHwIcdrEw-Erer|KY)bV74H;5^9o+8 z>>DdLer0B=VIoknZ2)ZXUJHY0(P2ksmksM=>!G)o2$q=eUERfigXtQE18aQ~2B_?N zyt0X&(a4-z)r3$=r24Ccq4$k0{m>?!w?Jxi-ma4W`r)N2R6^^SgBbJih72YOvHM9(et%DSeDw(7*{6ZO z=EEH|P`wF*=2*{&^obcLgq9ncKHTX#__R1foo7&CSdP!cX@0P(EL$soz&g<1KWwrI zZ;axq$~EAW^mKNKSck9NcefMMsu`yI=$s)9UyeU)^}ekyj18E&oef60+0_21`L2`$j93RG_qD|DHyPkv7FHM3uSN7E{^ z;5wppRy9L8&sDHy$b)wym2Oh_jAU9vnIfKe-1+SaUDuR2-x=$zao5@Ik~q~u?5X_t zuHYoxB$m0-zGyyxB61{c^i4JyhtMtzw7U9RYisv*x)x*Rh04*Dt zBqOWc6weZNgq5r*(g0>wgxwcnar`~6UP3X9^171yZxt#m7*ZoqetlHfVr2XH_;^}~ z@QQje%^qvzF#R~*a4z)={*`E%O0*EC5`#!ebbrdz*%|HCXiu4=%W-HLhkUowe*P^Y!;HgSV%E&Or z69zk(k2+KB4^4kEfZ|G41xhONKwG23Jdd}CKbRdGTC^Aw6*;@OsF+}LRvEI~yj9EQ zo`-)A;(C_(+o9`u?|YQ?txe##iLsQCCvyT;HYQL^gCh~qcCOW>RgQ1wuLT)MWDMC?jqfTj-6HMK06yzIE)M4N(n-S9iZkj zDa}8$zA&O}TUAk{HDoIaF?Yu(EjfOwG>(x#mvd@L3(f|4xWC&P4x7l#V0O-ZgB`g0 zTDF3dY31P5z2X+ow^fC_S+bQrcq6WoZg|n<8I$m>S9)5*=P>*3uf1-a4qTlcj~Q;` zMIwUM-}y~ugbmg*UP)f>g>K`8aDfY$tzW}vp!(x+59qc?fO^(qkH=469nr5a5K1v9 z5%#Cy=5;iy7&T!+4OI9xoIzlL+2^DsXEwNom5p;wmk;L+zfNs_vB?7gTljR#T(teh zUE*<2V9L6L)yP57TYV@Q?QNRc=+1LC?k~0r(y~V8rbw)PPB9x1**^gFp3W7CRiZz! zldzK=3Z`lFw}ch;Ub_RuI6Fox(mC^iynxn%RHEPZ+G@}S$Fz=tM7vdLm(QgNlS1Lr zc%`L1Yc$-EjoJNO%^p}8Ic1%4i-$9ZzKEHEA{`ILuC9c{=@Tyv45h2?ICLo%&+#Vn z`AG~eoSh~m){Ep+W_n|=(Qf+Zn#Z5&JSbZmB=9)jGRXZq!)CIu=+Y4j|1NeZy-W-?zVc;Y6 znM84`*Bi73+X-G9{ha!jR??Gi*AR1?eV+y3wYe`z1h#Piac<3pCPkl#PeV_w>l{A8 zIlBYlCeSzUYY&ySRbD#hvShuIKYgR{QKo?z>iF7VvEn7yq6;wWu!ew>rZ-}Eh%#!J za$;D#1y~<+y|ATnx|OMwL(_Lq)3*M0vNa;dbDSQ9xJJ5~e=@LpcD(()u(l7lmM2D> zA2t>6P$k_-%h3WU?Oi4rB(?Fpz+$8q&{mytvA~SM?G2-RMc-qf+&^Ehc`H{vr&L!h z=aE+L9aNc<=SXvz@3o;zv`UwLmw6l{M!+%^eAjcvIGe6Pc>HaZ<%(7QMCd`9mUL4) z-U#%$VV}icTmBacPl$2%H>?mhNu=9+C#o z$i`3K`Zh|UHx(9N-KcPB-wv(Kx*`|T)c~!cQ_9FUZxiFAo3X#}tsAp`#c8v7&?)F7;&{&@by z2>yXIjwsqqnwsiU*a#YdHO=u_@0nidpH4`>dkj)`?ms;S_OHUMrJJO#%r|NM(z5aT z5?Uw5Y`XWHtyl^3P#JUg&h)UFz~uT9SW{a}S+~2z0<3)z_8PzOYEr;>>R3iO7G9pe zvK;`~-dV!hWiQO6D4Lx|F5)AGr{_*uERG=K8f^P=8O=U9d19Mq4_(o7_OPl|0b{?S zZ8TUWL;Vu@Av_zFE{vLfFOE`Zq1SAu;9RGk0db;8zxl4cvQ9zO-4|SX&yr z+MA;@uBJqr`?ma50OL4aMK#)^q=}Pz#{8i;Ym)fmEBX6e+>#0tj%NRwI)Piz5`+-C zA{Ql@Fha-Ea|b z6e`gu7qzB{?WTrDOB4Zpi%YbzNT4O7#B4%!JuGS5o$qsIm5!ii6^KBck%fSiz@g<|axBsd2R__2EH`WDweA-yF_74(Ad<|9?6NCfdgAPWv?@heE?`yi z!`l4z#aewXQA+T_Bn2>vbeacE_%E@{-6N~(13f8(;R_6F?RVyxax9^<7$d$+J*q>e_tNOYbtKR5SF<5!*P_&OL$l*$rk5Z$4tO z^*mSd&Bc=smRcV#@d|l#U&(YabU7OE4bpgDw|>Qd;tX(L z`^`Vp*Q`uNCWvA)gvN){+1yiYRnD(ssr%lOf_SvUqV#L$}H-{;{+#unteQsb36^^ zD~B6X^5|(y*XsyV^sH@66R;}c%rC{pbDTPLX-;w$iZavKs7nq{X5 zP`wGN&K#xUSml(u0?c^mUj&STQ2m9bO0>(UQz!AvV>{6Pe{O4)mj7ed#2%YLk?;yn z^b1qmmnGKgp`}4cK1`>_T~2h<6qrAD99mNC+F9*MUfp;0tJ0XM^rlRcI(4(k2jRbw z1;P_9C5yd-iUu;dx+SrC`Y{r%CPA+1{JZkoy9mQa`ZytbS6!>9XjpzcPQ*3y7@zvP z?v=Vw(!6^e41y^9iw%r!$>!LKSx*@Xr@)~cWjOUun1xvoq4hq&a_#MmuRGdlxUjFO z{Z#y^L1=y_83^I{P0?S>?XL-BSZ>{qVzM1<6`F>mSGfa^?0n59`sa)BYcJ>}f;p9A zwQ{C2r|08`Z19`o=*f7`C6O3kQnt5EWz!Xd$4aUa0_H*AJkG0W`)oUWFng)LET53G z6GT;JbkOLPxxN-?wBsLKplmsY0EPHx+ZDcSxM0OYK?UMrG<2j7>2<1jWX09d!W-Q+ z>Lg}A_EyMZ^V3%L5<=_aTKPgQ?to%WN9_=6K}$ke^N_;b_m2)QawDNT&n#20DWJ7~ zVxl3;;d8aYQcoW6gurDPFSUw|CI#kAA7z4T>SK@dX^)mKBpd-wz4Hw_@5_m>y$?GK zH(zP!o|W3(jEfA*u$!{UXuBlWd2)Hh5CPTvJ(~X0V*Te6OBNwr^|>+q0zd3aV&o*I zYS`j#$DRq`;597`i^pjX@9ijc_!3;Xf}@Nldk<#JM4Bq_l7OoXC7Z#J@nThVX0tip zJHyHuP%K)$N&Q*u;u6d1Kk395Hi!TZwcK)0gS=>G;qE$BE)DN#z0}&~MwaAxKW9*? z@3PC%z~};|^R>prTo%GHLfAJB<-1gteP^Uh^L+(YiO?;4*wfMnHJ8{lV)InjaGMyt zqZ;qMboqie(r1U87r|xxQFlMZ=0q1I7(N`LZ-dsISL+p1-UF{#21U-vmyYAp0qx97 z>LVNXA?wbL#W*kFu}q;9ZLapf`>xwrO)4eiTBnOS8@@#DYwU}t@{AUR!eee(BJL_j+c@Tu8$5DrhQVkH!6y9u)|P!z%pnv3RXCKZd>a!kjB%?BRT**W zJ%(L}nU@n?fecd?3Qlh;atK^+DekS<{&EPtf-$P9{EP_W*kdtR8KqI>Q~5v=DEigT zcUn!`;pbT=R{8Me8_ZiPZkaGOML|U0;zK2yxFf4hlRlu49%RQQt0avD>gH=Y$N#ZY z2O>?-G!?6;Z~^B5wvV+U{3DV^_w%U*|96Kw@|>1@I%ushM8m2$W1f)a1z)O3AgJlAK(OiH>s z;Y8yJxyWnuB6pWWebzH;P?*ZdR8o-CLbPWBWR`=QjFAJI!6}0OmP{#kTzXv z+JxdT+!?UKPmyrU^cI5IC=gufR*r4$iYvRTt#Xl*Ct1t8-2P8p0`A3U9pKAi4BHde z1=B7+d^#NyX;QKMS^-{RKvolt9b=x3*W5Z$#%}SbU=dpINKZ! z_qYu6Sv9YBc&b6QMp`EjeK|l&P7akgre~YYC0xWiPvf;wraJ`~!42TFYp7+7mS-Ef z6hQ<|8<#V2O9sv^D&I4(L(lB5U;~q7(hzy(YVQ@^McVC8G zZ5gRyv{-CCfslbJL!p5hH!Jn(a5*+}?QaS}KS*800IFPkKdw77jKe=3C*WfTufh*- z@?1}*#x}`($b>A}POdU?eN#|*4$|xvzr{Nl% z`f)IySANsh0YopW^o_|?&?3X8q_M8ELtR#?Z%JyX)*5Kj5BGq8esrftZ;P9@VBXuySOM6Wg?XX);}0Si-}OJC`^9Xbdh7w$ zqzKR$@?-M4GEh8!tb6M{CDK{8D$msk*)>yPG5YRdjv}r8xgvBIsvyuh0%!y<#y&*H zHg{4;)u0iU-Z9>qhq`ew)(eGOgE@@ErX@4Vi_J=AcwQ;t3g^q0Rg1AEt)5=73A7Tu zUKhyBp5V2TWEEHcEwvM7URCVsfSfnGZCK7Eep`+wCjP5mJK3Zbtih>NtS|jVNi6ZB zqw)^6YDZgZ%fJC)#PlmI|4Y&AfuAFk`uH@(L{z)wBzvq!cNQwxomDN(R(^IpueVWG z#O)fh)rsXU$RO`I7wDVFA$vI+PRHnqtAN>Fh+c)@QDhrj(}RpBcBWQp1zbmNn8!fe z7JCbW4!lJ}Pvq((mn?$uFwM5nG!@3vE`%A`1aC;=R*Mp{%ua)P zVX3gD@Ma38XJzHFeQWeid6|5AIiy2NX)&NoaddxxccqL%zZvOsvgm`fyr+Hl9i}C( zN0Z04ldZ7Ik%#bjTT|Q~D!=iiH)hBFe!r^6V-#%962b-(GvDJVoo9$M=tG3zi9PC9 zOydKfViGtIaJ|->YY5C#x3MflJ#dzm)Rv<5waLFXj~AXV+FmYm%7c;po_+8ndc$-A zF%|?-X`|=KiJE9LsGcWc2OU~GnTs26s) z55#qLOeTGEEIP`cN#Hf1`AwBzOpob&UF%ZK5^^yv;+#G^=|Q*FtEgG0=K{#tr%hSW ziH@GQf0EjEfVn7Y&!%}+arIi5Qr9WI37mG%{0!3RsapQ~T9?-=s}v&jVc2UMnslEF zgG@dKF1F&Aq6kGj>5AH7TGP%k`;Z9Dt#o0JK_z#532n>LpsRJ*biCatZ*=zGF89%4rJ-_%odeH zgM)+9StptzkzxtlsoBC46%+`(zi>13uI!O(U6(%5{mMJZ@+|bk<1(bLES^QE^*5^I zc7ZgN$!dh+UOE0%@&MGA%T}sNz5!OfZ)L%iBRnTzI(%X(+UhZ#W5;qG`yj;RL%iAr zFRceRGG?du3zf!VUx48yuN(DFfePQ(n5!-gbBm+Zcl-oTqpvj-wCK3

6O|b$?X#frKi{k6G~z(-O{&x@R?V%Pk**PC;?v%*$KBN`BhT?(ZXgi21~bTqO3g&)VQUBL3k{BRmyWC+iQJ& zTEL;Z-Fm*}$?tHAIvcDF)g|ebt*UOZEu;wF~m+sw@it!+vqk5AG^frFv`&7&rxs zX&3uX5buE(fP&Bj7eV=5l)ysO1y=vKHkq>gM_B#-IE>H$<@=6n_xtklKuBL}YtF&; zKNpeSAau-o>bthm|HdcMEGG#&e8vaK2;2p(fB*V5iIqR|aI#=G_w8o`ij#$|_kSRR z53j(c-*`+~i2RQ7Jw&Gne%@(@+TDx)fqIkG0WH0(5E454`)T&T>%kNQW;%_#e``Me zW$}6mfJni%c-Gy!4n9i|9tXwA{)a^#CxYV~`H*&ZH*%5*v1T4}uEob>Z5bO*!03Igy0MDOFqO~Aw>Rfqo7d0U?cKw{pn(DdX- zpV|3S!C;C)R-7p4Wje(%puqa`pr<*jtH@c+6Xh^sRkN3lH-5Z8jDTDxeOm1wBL4r4u*=8lLsx}k4$z>T5y4uqt{6i>{==NjTS zo~H{K{$ao5%i!=Y-yRks+Rcvv)Q`ifHwo_B-QKqxE&NZ++Zwk?^E-8UbxA=DjGk@T zIx4-?M!cx;BeQq#t5@?wSfATxyr1-=Gla7URsX&bWgs@&2+H<_vDsSKu>Hea{ zw%g1i00JvFe9hT4KqwT`Y^k-jvk*t-!&n=FR>)Fo-EYbka%sXuV4v>+j_KI*qGW@g zrrS2QmaGGw?TIm@z9GxvAaL4uFMO}D!?8 zXNUc&uG~}*K{^n(#EPaEk%2#*pgDHw+x`+;sK6UyO1d@Ae8==Re1HAdjfjW+e5H(E zV0V&?^Hg4(N2AvET|GYUxv2ANpwx!l>8_QPC5S;D=wDe7Qa399us!8Hl4u?e6%g@a ztD820Q~fv_>DRLz(xkuq{cQV=fH(Vo5gnHgOA~jVy8?y}r_}n>3VU^`PuwZ9aZR)- z?uXY^uAHur0Cz6uFuaxYAf~s0E)$n1D0=IKh#l z5#lD&SzK4pcFjuBy~tKK zdhX_M%`*wiCz-WTIn=ZT`_;vPLRNQ{p2!^$&sJ;9RLm^ffwk}m5XVv7!4r#~s*+TU zyHGfRIVpHvUx~L27Z_vF_`Cs34-Y+o3`g`QUXs~7L@aE(C@d^Y=gl?q=?(Sy+z;P^Vuts0#$tBvLX6YG zr;aD`?EZbG-XAcLMN7m^vz_4_N(^n-L;hV{4{`Dv!VjJ`zrosic8FOa=DifBaL+xL zD!K|yD?ybszbI`xftlN&zKATE$V6okHK! z+n|j(eST!bV54%eC!e+fhFY8LvH)#Cv+RDbkv1yP7bY#jy~(7)hi01iq$o=VUOKge z;}3UOapFzwTpB{zZMjay;EGf1Er!9Z#ProW5hj((<5l-|(g79_^IDm0@=M741?osH z%xV{2_j$qr8Nb}d@(eNNw1xBgzH|O-qx~%BVYT-@X8$7!z6pe{|B{lZGv)1FgbqM$ zomvA9IPK7s9k)2TXg_8nbgz(Sc=J$7TL#|jrLx|?+neY?dBcfh6rJpxTt~`}$zeL6)EA!~Ev%wfGI`jdK2W3AbI(J(m!T#r0rso0bY@Iby_@LkHNMwrC-ESu z@7LTm<;&@#IS<7k=(3WvWReS+DSq%_vsBmWVRx|STL{))G310D z3gKP3DZ%-$FLXhs2(e=DBBZb}e$t;$QrWI@tCUx=d&9gQnOpUgLTb%ot|zPbByAb@ z&i2Pdoj&0v$ueqGl6*+Q)krug)LAwN;*w3xZ$_r<%lE5}Z!$lzV(h z^0cgiQl_@XiHY|aKqa4ekBr>SxODOd9zk%}$V;$Ocw4nAQZV#G81!A*4U%tV-y zvIPHiggO&O*ZY0XQj&>>^@q~=t`X10$m5)tjigr;Lb68NB(RHIoUg3}Fdq}VK3v~M z&mYU>4L`y6WG*Al!)0nT$C-sPh{^3s{1u`ysKvCBxe+7wi~6GJf+cK!`E=06ty{^* zks?EGHd6{@E7G%9V0$xbBTzMkJGU(< z30KyQ6e*jexn5qm^vLSnNo4Cq+|ZYYJ8ckVIkXhNn6a624dHx* zSn+tyI|JQ%Zg4T)qzK8#4bY%|{V+4^Q-_d{7(3*=p;(*4(VZ&MsH#kPs~Z7!^^&?s z+|{y=>P>cBCmNoNk@IZeqw6xl(bFPy$7n>05RW}}b{ zJq3LuFZTHmK9UAlDwI0C;*o#kITyd&+r*FFQKIjvT{cO*AXc_BwHubqN@VAzA4qa#jSaEIjOgB6E` zf^h2bAjq5>z0kdTEZm%6T)0q(L9uwS$Eq(gbU!O)z~pl_s59ckH5JSque+gh^~Bj{ zRV%0Z%=jEAh1VZk=zva@WE{=l7nU_Dm$(=$V#j1tIC(+fAvb#E?4p6!a+FKvZKHC@ zOY%&;Fzn(DjKlVHcd?xmt+ywPvG|zi;!KiVo^#T zQ{&G&K2q?BBrC=YIEQ2~Q#54xDN9bCgQ#VO9aiP>4lzAg)v3#Lu_060L;HEaBy?1I z;uj+IB@pRH=MP{)41p2p6Qt9gEDPkb`6$Ieo=w3-{K7K*I4eKlDH6DNGtMaVTYl8r zBw5MP)d>|xycK}ZXg`qjSWE0udV=m*DxX)!%w=6ysyc;b4L#%8*su)oSx|1CkR;hc z-c*|F!|jcl0c|2_A3sWrO+637ELde14bT_-KYaalSd?GX1q=(Kh)RPXNXMus2+}nm z4bmbF3Me4mH8V&EB3&ZgQqo;BN_Tg|&_g#21Mki6d5q`#-s`owTe^9E>C!zZ3V& zT-XUU5Pb9Q1In2WQ2S=(G)#TL!~&&BBRjAabbl~swgW@z&j*ZG1`=9SNe@rKhKpsA zDbJp}ajXp5(d~QDE10JT^Wp&ys1wO$E2xV)W{bHw-C|kxAHt&irWzZ{;pjl;(ELXv z$iukKnHg?Tw|a$`g1YTW`?1cbWWvf>x6(-ub^UlqL5bV`N;S0pR1bX82UXIW<)QPO zYKJ7F`6QK+&dcKsSHdTf9&@~8U*R*9O;EG684gfkQ3tho>^oJY$ zgX1U}1EceBnFk6IHwzbDned(p?2vofNx`+A+0N0&?6CU`o+=0eX?Q)b3m*6y4=+}B z;`v!_j%U%JI}`+cseS`#%KfXv_<0TGtsB4J`h%@MNyB{iB-q`1oZ#_?JkuWIgj{Av z!b{fcU`JAr=eZw~`TB`xZ3T0hO--j;*+oUGgIbN}E!OLS6x~@amu@um`!DvG*Xx>2 zdZy0?<|#s*E<_=R&S#^>m&l2;;+6LF?8~BVS0wDw^j;qEV`rzzr0eFL^)NY~*DIQ| zE?e^^X?*jIrmIW1Fr~l`%d7{Ql37ogTRKtUo94?L=lk03n}?d$t=%577Ty^#qN5Dl z=o;)zFVL^8dF%Gmad?=Cl<)8$uNYS3gRWD`@UmL{RLvpQ;TILg4ebQhd`8{XUWT z6Lqt9u(t0k&@WSuz@kjTi_shp^q`yMx$@|3mm&|br`Xz^_ zZ=)4ey|WVFu#x*K{%>Taq47feVmE{?-)K%iO1G=6yBj_o#N@FLrrz8PeNc4yt8AId zLdbB>*4I zmMhH~uAy}auunM=IjUO}kJL|70(EU_IbRw__PztF&(Q8x3;0TqQJ1_w#ctiI-CA3Y z6IOyyq)ux-=(VY*6geZvVx!~bji*F3FY68vI-kAmQ`p{}5RiIQuPkJQgCKTP zzLZuu6*!oaI3B37OUChR^e>e5e*lp@dGSl)yroC^q&rbcKIl*Y^DZPTae&>aU6fKJh8X4 z+HeO(OZzzkaDa#eHOC2aQ-AdaYXtH=@Dz{^1@o?^6MzN{v?o{^G}~~5alKu@sqrZD zOwo;^+$KAf7-1l^J>ce!R)B{8;Bc2c`shX<1)DRblMK*5oBx&|%mNZKFDA@elyr5w z_FYQJWxapeQ`_XAKHcp6gv0US(9!Vb@nT(s@WV8$-}Iy}?Mj%CG8Dxj{2!sh7w55J zkmqr|i&+YV;K#a~VMJ~5Wg1Y$} zJr@rKds4T~Gog>%Z2Tf|T;&d;O@O4ck}GufC@WJGO=YAjS5l_&be@TkqKe2HHL#$Wu$lGI6H45%#Plqa&KEuhH3a^8rt}3I6p1X?bHNOA z$15j)wWp^uD!NJco?eQ;yy~b>la=7E*(_XS*#2d(#^TULKte(S`w|HR1r8LWLq#s0L3e(6u-`j%fzf)dHDzs(W**G%#Z@u|ZAvjO)U{PovGJ1nE}C=zNst!Nf!N!|>UvBb|7< zmy?^9o&_Q{M<(uv{j&p690uw0sO_G;OQ(PXg?poYn~3SLwKf@m^=*3|84uMWJUMVw{a!u|Dg0?QpF$!b7SEiYbE1zN0(IP}U;JvfmqO zhfvi=)%m+;wpek&g^7CEP`jueo{HkaI$4fTD5o+$*KaTPx~G%^Z&!W_8Nc;gE$bjD z^1$D#L~LN06$hk;iXIU`Px{e zn~G*~ru%s|T>Us7RZ_?x+R36281kRBdThWctg5_b`Oeo-a*iw6CuOe1gZ95pDf|~_ zsZUcr#X{!1$xz2v3CP;eagJOz2frbS|70y(kzfWOgJz?2Tf_&8R7wfl;Z41*bWkz- zFA(Fe$$BSuR@mz=)c~1g2L90IZ9*F^-xN8`R39bh@Ucsy{b)_K=J6aO}@?R&_A2LEOmvpD0h0Q-*5dvTd&Vz{z}sV=9t+6 zKF=*~D(D)4SGQ=^tTGfA9SPq?7`enfM=J1*WTHE`9j6n`=RWB6TU?&d40r7lW}4Vm zB5l%sSy-YDwrWf!pN(h*>`r?Se)r*k^qkTiC!^Og(VkmCK60FM+}Dw3*+j@1Gw>8a zmAXK#R{5#P75#Bf3AS{o2^^GTxO&hcL;hEbi!)Ofu5osuZRvQcnop#?=gzFtC&!3f z`Lv3{^9n+DDj`k-)^fR>y=R++p6KVv%+6YGjp}|V(tZz_)3kM2Bps8FPHQ@;^LNf9 znSB?Mwu^QB<^Yg&YCR6>(mKPb4Z_Cx8R z-jp{@f+eB8NAl<8=B{Fkwrd~V$(Z6?zjt55X7 zfQ<;5v^qTA#st?fpX6t#&O-5b_1U#`94)Vnq2z@k`QBkp@_LK&3i*nQYvRpmHEFhC}*j~CY-^l1gd9PTPavtEY#|{ z-X5|gmPQ#sJ^7xv?D=>qVFaN=DU^m-dv) z5Aujf|K{E=hSnV{MzsMj`UiA^y^zCDta^EZZCXWLudz;m;refb0`e>%?}B{ax%E3_ z{g#L5`($yB1f94Kb(@n1GVpeI-x?@=LaZnYk@v6$?Ad#x<$a|4q=k~i6Kf6}YeMxr z=(|rQl5>Xvn>s2x^PF)Y?v51@B_6HOr4_fZUN>xpY`+s4bSZ_)2XXW&>B{*U-vA;ktOrr8d2iwtb!-C;* z_yAeJE+CeEA5<2K$vrI}S6dwZrSSDhyJAp`e`_lp+BhO|VL-|t^J3a%R<+Qe9y@s{ zoD$LTSYqvSV+h{-lRNnfEvCuPjzVWdmKGM%|EGQZWPKka$O$h1$Q_2*pH3m~?rs*N z=iZvkL0{wXU`|$ol!(U(hRe+&o>|x;oMUR##nYrMEY)#w9!5u>n4NnOxbC-M@wwN& zeIU)4G|0;6Tn>~Z&9*yvlu>s&LVcxh)i|Fa|0lTni9X^8#ef_<+RFpaYksY!1LQW_ z5`hUR+x3f&fb=sILY^b3Yu1)gdw%Q8rFc6=+3j1eVWX=;{KRg;pni1Zf>dF#av()A zP?ac}Dz|PEd1A+7;#vGCB6ky`VQzE|O`rtW2niBMCEv0(&mcpE&*bRWwFA>y}N@gU&uohjnyultxnT^0_2pIN@G`mhoN%$~V8KBY@y%zRXk>JUWu z8=|~eK|q~~RVE1U|EK%{e7mmj<}mm;b(`)SWP0(7t#a?t&ZP{>%He@k^$JJA2DtYk zb_TY!?u))?@9*YvD39+Mkw?G3N+va&^mwG9>qK7`_O@Rf!2chklka62toA zV7fT#@#jMo58symkDXbCi>b4=WE~3?)Xq~0qJma^-Ta_S$(NFW6dbq52G5+v>59?U z7E}nCZuKZ|QvtEk`kz?2SCxm&coC*gl$qcysK`FDXu)I>P zag7w2ZxF+-ZjeBurnQ1 z1hwdT$PHWqyFB#6a+++kXd^@APjS|1pUSCpULuupa@7$tZ~Biv+?L z^~ClMne5(=<Y#t#@Rko@PjW~bb1puE+D(m-)=RbWZnz<@1nlJmM&fvUZpTpVev`y$!KA;^s zF5h2qFscDZUns#;&d9;fq6c0D>@J|^dLnA0OU&z@^|!m|)@qj`nR{;+cwVDyWCV_s3=V|I!LxDYf!?n;6>?v|&L_SH0?h z_w;+zrWtzE*)i?Fi%zP2WdzP(@fr+0$-e_$uH=xtDksC|`3wo>uFGefqslc`lQ~3J zCXUh{R$M>biQqXQB7L-Xvpe3VdM2*A0+{Q|YEh5_wb1>KUT~rPqW=+k&)E-9P^$S8 zBUjS_tUC2&lsDg6RDC`~Nz$$bT51 zn2xDc@?9GyS4da-0nkpM6>h<2;<-qqL~9rw*_PQhiCpaS&VjgkeWgJlo?D&P=j&@8 z;T2Tvg%(VQLGkU1P=v=Rio7#jr*V9qwE7O4(FIoE|B|=u-#Pr@9THm4f+PwX|`DD-)GMl+ot7 zkqb^qS4HsltAIKchH3sTn_F=lQxwl{j@?cnUv{uH91nvGtT7u3hDTg9D5&mLPbf@> z@#uoHlAQ^hwnwJFO!a!@70}871!cTy$1g^a$;5l5FBHzYQ>d-V@HXM7B*6qgJP}bl zjb)UA5r~}h)pJ`NXyWf}Y zZ2!q){=C3ver8*qZx%|5X}^*U)mnQ*5?YH1G2pYs;C{Sq84Ti5#Xm&f{9?+S`%LY! z!%$;vyURbP_otIyrJhmULz?4B!c&G9bR zB~svMeL{gUY9>(fog?-2Q@Mm;8YR-=KHXV6qb^Vqj?#AME;ql#5D;YLc3#(UyH#b| z{{og>>n|gN1xq0Um_4?OFf^a5#E}g1{iQ{^WLl-{7zFSrm3Vo>{YP68 zem@#OvV=oKCxN?k{f(hh+h_RWqeQrkegs*K1R{@5l6XA63I$xOnI@%}3nR$l-&m3& z-*vI-DtKO;pEBd^P1m}tRQda9nl^HvGzFK-@*+bgl9jB6{~AV7DYG^5o!5^v6oesqyVeSi46P&B+~h zR3YMR!*O@zjM!k7vO|rc9e$!QLLE7{4yN=V?M{9k&HM;fd!571nPfi^ zF&XiJ!c@7%&aZPm9T3~^NW0lGf-c5z{mttl5}?T0OL^dtHFAXgtnrr8wOb`J($+V& zzp4AavZgz4%k#GBaBD7HpQssXw8;Z)(rp&=Srx$`>$Lz$*;Q8-2y{PMebhb0Sa?>j zzyXuhHUJ1ljBt_Q@+xV=6r%!#o9*!lGPDm(R~JwF;t6I*yW>fy|$08u2EiGT~^Zg+e;Im$pFR^Xn9DnEI&A zyvfifh^^Pxvn`A4dPPyqal|YnF$uc$XtW%!1*i$D^3Q#gFzX&3RU4c-19~3X>edPp z4DC#eO9OVHXFnuc_hatmo?3ZjcHq+0Z_kuy%4fli*A`Ef!pS&_c^6$II@L$53d`H5 z>L5Y%k2|DtC}ojGn*#_+ zG4I|)rV(nqG)o8%0WAa31*Ns{J%^`8qt8T6>yn|-KVde175JV{Fm(1sJ+i6?mn{tY_eC7EE$-cFLbUGT>Ez$tyi)WNP25yUd2fG%{ zgw%s)iv2ch0lN(aKtI(L<>>z@?n6pwWU`?vDsG^}G&tBG`ISB1=161$R!fruJnTFm zRR`wtb8`C}w5wyGG3FdkBTLIHFNY}6a@zagb}}BSY#kW^BZ?e}84OvBZi+j&{2kA; zH8eE(=ly9EeQ*mSv?>#@kI(P)_T7uOSu-Gha`Q;M06JHiQ8AK8Ns(YLq4ZWT*0BAF z6ePIroCLtH$J*-b=Shy*!E~_c^&ct7_Wia+*6*Iqbz>_EL{S6>&b~ zp7ZA2|8B1sJ7^l3(PXg>?X)(f-xX_#g;KQstzza%r7OdBN}^x&gs}{E7*FX zHxC8$guwjpb%&w9z==(49&E@~Pr0i~y@L)Edd%afBzF#8Z$sv|6wW+OD?G_K6OuWt zRGYv-de$G^#_Tt*cqj25O*k0pm?j=JOm4n{r+oKI^0+W}PmVMj%p^gqBtVIsz8u9` z8r7XdKx2U_Oz2I5&&q?N0{7T;BzuL?CR=s6gOug0tsI3Jt^w5y27$VJU6F zXFtX#6m(~l7;JLF82ugFL{rZ#JJ^#2^@m#l@^z^B{`3iV+$y)_eYA5&EBA#D$UHK!*E!cMV7K&pa60Z~NsN=! ze>A(MvznnFQ>$$_h%#nR<>3?g=vT0RU8jR^#~O3!ju)TAhM%=bZ~|TzGLgvL>Jtb8 zXjGs<)yj)A_bCPOiZrUHBAjs1DM9BA$?LcT_Mk`IhR>3}??|w?kvnH*awH2f7czb2 z+Mm479t&R;SIQKP@lO(_q!xwn#Q2IjO_0S2I?2yp&t4)9O_Yc@TYCG3qQJE4+sqH6 zQoFR@YCV15$$!Rn(*Kj`PokZP1Bi43^&W(ke1oYHxJn0!VnDmyQz0vfW1flqHIxr7 zpjwTK`&*xw?K(w7-*qes&aPw{ znFaC0>=}-73Pc%;U3vjmJ2A#Rn^(wP;=mrtY(wM&S+`nmQoZ?1-S$*(8jHf$q2?Qo ze;^2WQZRE%<;Or@qCao_(Mi^oQULy%qi`H;4wUz^-_LZ_2tB)JPjVh1b;oSCVLPY{ zQffM@k)u93!_Mj1zHnm+|7)bQ?#m!UB{NR^-+Kj=QpIlIP%)k6c$=sEOP%|$Z;sxJ4y<`S&G-r&-;ofc=Okg_|`eKJ0)0#78l6K>Oh6#!ded&43p`KxXEg^tiaVj=QNh{whCKL4b`;j@@Xr2OKAk z4@=|*{5t5T%i{I%%vTOC>N*Kmd|f}ZKFz6T&boIeO`DD*z1cykHzqK2t1Vc~IovR| zE!tVQyX4t)lewgeC^@Lb&{@?+BdDz2g}lbKI#SbvhNivJtQB>zUz(&}`2(!6D0$j@ z;;GhBS4YqD<^P-wEnu80s$O^h+N;2&bo{ zEfbUbVC|cM3DZ;(d22z9%oc`EhxDgnqRlw;^PJPzpV`h{f73i*60!SbmeUwhiEN#J zb&B)e@$csm18UF)aw&g35{pQS7$@w!j4|6->7P%SgtJrocE67O^XwM zC~s{i!Lvr13qbq{0%M-Rq;&DFi;RgDvJ<~}|Ih$BCI9llyB@ULqOV6U_ol2;R7~Vj z=G?1OZkmHXC^^A+^$D8uLU{iAsMtQk$HlB=ho?k%e{JlC8Q{0JQifPv|5I!Kk3QrF zUiE2>9yWad_9O1Tm+j;8u*agra7SC-ei6;VN45)<`P!TuVEqkK&yt{fy@%~PG8+?K zeut*Ik%sUfj4Y$u279l%rR^HcZoqCf`}v1dH1&i z`I>XYY$5r`*Y1$13>_-Ix5WxyWB$dU3974=l(U5#UV+>He2MKdUaiDQgyZ|{_g(LRn25F!AJWIFkMBLWXfYo*~>otdfCiY(8bwRhY#<*He zw$G%!JXp{pkb?QH%S<~l_{Vn*e$JWXA3dtJ-(9Wwd3}V}k}k}Yg20=7T-q0FW9m>L z#SafF^LFL)>-NarjZx%f8(w=Ts(KMn%)_8+Ao7p(c#XUS^6UK5bt9{PKHfPV{b|mr zA(Q04;U(}Ij8D%XgOl+0a-N)duIkwYPgjqP|KKc(QxcI> z3Oe2uNDXAm+759y#Sa$BB?bKy!p#K#a4Yz}8!tqABTVi`=mMKyfPqp(j5qTsBsc7j z4VCF#?P&EVLHIuoVnGj>)rc*5@y&l6{J~XVv*T~L^Y=JDegvvEu2XndC#iyqy=8L3 z5JA=}w+yI5ACd%5^qcJxI}(l`p>~T{d#L5oVh;-gRx3gjNdk7M?TpTE27jaUdJ66A=Kk z`A9({^1naXr+4Kj)p~eM07LHEz;mQllbXI+`5aJv!)PdCs(Pj~xJjxcH%zcA6L7Y_ zkbB5&nT{ZM%3@~>;3ivKar?0@JdR>Qf{rUk)i4dM3F`iL>0kMWhc)L(k%XJe{8f=7 zOt^7Xg%|5;3XHYj`-c0QrKo=5oXOoLdvExVTeHqe2*tyunIA8>L$&FZB6`zr-v<^N z_whVv9Z*Pgf<$kX-N!zgH9sDS=Pg}ds_X+~Fee1zu!}Tv%V^Aljlb&yjLhd+L^72R z<6oKNqZo=F2%G=3o?iWYO7&G+xI;C`XP_-SWt?>NQY2Z+;9Cm&bv;XOH~3FEV-dxw zkPVF4(O^%W`K0#Kuaa0#Vigi7w~hA#c88<*Wkm)8vyMUeLY&HTkzNhO=PYov z<6&w7rKhY*lJwy|ib(brSKPCMQhfcGMfI9Mw5g`!F;u8kZ zd!hGrebg5tUHkr)nX#^-A2vJ5?;#{0JM?<}5;)QPlLaI5zo(}nb7i9;KU4m3J9?D} z!0!|me$ijqtt&Q-`%Vr5CArq8N?We@%0@EX zgx8_7LeAI!pkXx0RmK+K{dZalFbfu7UkveBR)K|;eZWpcTfMB8en2sz^&Imw4l|#h z%7LFuX}I)mml##R5ipgSdOG+RsY6DdB-yWONc9vZL@-C3sc(DtGn++Wo*?x_Cid(Z;7Qm zB?2H+x2i+y4|ThC>y`911oi6{Je|MCdks7Ks&eu!AqBwXzT0>wz9+^x>a)S5d4o7d z<{bL6cMxuq!E#2B!t!*-)mN+#*=Nd$i?j*SZad>|pQqqWSdz)<(?DdIy5ht9LV_if zxMQ3k$8JJ8T)lY-F0M`qRR%NqJ9)uuTRIDIc@9SBp{GqJk}15;6OcnoRdM-Kw#=HI znIBN>n!S&scGy0^nen3l^;%!!B@J*R@`$izFIxsi#7lh<|0(yM3?z>9YQb4~UjIA$ z_@BdVUd4>b+GQ=yzhFvsm6J=E(BuB$K3GKAgupM4`cqi~zoo;CCBgt7?4aHi*XE91gvXv1?)Q6rlSf_8^74Y&3o@e!rQxSR4L)1PB=l zEzP)7^8L%BDW1N@O@K((cl=Nxh4J(=QPqt&tlY!R_AX8iu|d)KfSnZfhyf6X0> zNCj|EmH(Sfn%%vMKzG0FTp=#wEwW^y!g&R00gC*pB+(;AhY!N4h2^=fT4el#0?{fq z&8kUd4iqF+>Sz7j;Yz|bPh=~YcMV8>HlABIWGJPzl0uH9AV%lnfCw4T`rh6Gkpkxu zuO8l%Ubo+^hf-AAra94iw6IDiV^N|4q^H3Jq-~|`--a}Tf zO%fPx@Rft@%Lt{Y6fNFfK;9>U-)HsnHa)x0&llGNdno29g>>wH$^-M#c@+0wEBIhUQ2Rsq*z)sH8uCS1`#Yz zd!vonfB)MRDldzr00nFIpig%wc>QgOuK(ULAYc7?n;8f_n)HyDe>9tG*xf-N7qQq4 z>axXE893qMCvSie$38v0wzZwa*E8ST^F1tYarStGs*OZyus+v6MBGP{>a+ns>TYiY z;i3D=o+BLYHe?h)^_el|8GCSsI5%zLwO;2q`t6$obm8??ntVE-SBWpk{7oPZp7RHf zb7PmwD<)FDROlE!*$>25$4s8}x{L}+0M?P=Q?o3jn#0p&_u$hC>C!kWwZ?$g9vOkze!x!DyJUnjEomgC<3xP|whfxM9-k`>i#_lPKC zOlwF0kWWz}h33+_D#KZB*=kDhId$E?7RPOhU1G{_+Ou)<)90p>oAW*Znus|t(c|sb z1JKx$K(?*y7|Ikx+L_+?>tKH#Cps)(-W`&Fa80+q z?_n$A6vep{X}WOxxy;QQN3RoNLAQC-RoZc-w-3m);-aGv=7I{H4`Yo7`B^X<2H}FWnP0 z13n1rW!)XTkN~f;XfBFGA0Z(-t>0oSY`q~f%|3tlaf;lnAK5@pkoMh#e-~ZGkF780 zl3HKul;KeG>U7M>Q&BSwQ#Y};h~I?vSpN>8IW0ziT9-!cDQPvfsC#*7xa=3vZvy)ABEVrCTG6|GeLn1(A23W6WLM9F{nxOi-KdEMS?vUp>*CRXcR!=tuhg8dU_Q zt%v_LB-gN}g6&EEnsk26ou(&6*!x<|f?qP-_3cqFueB5me4mfPibplJ#4;Pco_?f- z)(gabYZ6nhTEgeMtFnTQ$=W@>Gb=N-W0MiQrG8|QJ~|At@RnWpExq;tW3P(57ltE;PTen)&~A(7sGea+D0qw z8aprY-Y*O$`+Ugxum%by9D|1fS{5lCnbrEw*$z(D%V z&tndUtftyT27(?;Du)x%$>&4DgSsZSEQZ_Pi&aRFhM0ic=9!8a_cxhs*^XA41ryE2 zBV_B8@7l`nU8}l(u1#a!>Fu3H)l{z7GzyKGINf|0xrc4IJA;8{OxZDM4STphets7a^YZEDR-T{ zE@P`-O~=%;9|LeKUbi*I6d{5BI=G=jtH8B-1eB&Xi*`uKm|cI8y!iX(S4R_ah2AjA z@?&+Q6us6SQ|wOC`1wWej_$XuOs!5Kqc=@3QdUAg>}nX?n10QAgUOr_Po!@5ta{X(W^Fr1ZplX+GogDUIy&1&kdGTqvOb+gq1 zN#DzmHsXc7qXBv*&)l(SilXGLE8ozvjZ`Jn)3gZ=sot-4j9_k21}H5&)Yl;S~C@Mh_aYiaOx7F4ZG?RpKD!Pr<0hoWK~;M>Fa78dqWZ= zMUQLiT01~+Aog{W&$xt$*mtL2ZbxI- z5l=argps0-6;7HeGW7@sTl(&h^;ikjbRDz$o=swN2HnA6pB05c|W??lqNIYu=R-j{qtn1bw~~CArf8lB8Tmv`1H%D zq?C;KxsT-$;frzahPvyjZ!Y%)BtD zO8)-rfz7#I<90l1Y4~A;6XcR*X?Iw$&8=ZB6nAR>7MyUy;o9&hWhBN`l*Xl;y=lZr zUDnJWcwDi#mRveL5!=Wo^UW!pPq&wgcXR0S72;xITM0nt<6aS@l0M<^z(}cgT~1o$ z`&)PLTZb))wm>VQa4?Cx@YmVUgK4PY87%Z3*=fIQ^Nb9t#n{fdA;{bPiR$R-*~B&b z^NIO}pAH*eBzfVCiM~@e-`xCB#b7t?8o}om|1zsT774cjp44_PYud~eUa$kZ1Jg>T zIZO6%kBLY7Ust~rl@3<HvxNNj6IJ-MN+*UsaTHkmmzX%S!*>Kz7BjX|I?X zq1iJa2{5C@5j>t8V`*|(uPweK%X=FVK-J!{%?vz_Q?F^OcgIZF@iN>pNwVN^ z1lg={@K`9R{OFAo&$c}70psGkD?xs)4G`6j0j~D=-46!t-kfsYKMkH1pp!0SdcUjf zFK+}9x5Mg?RPBc?9^n?ul4{#7#kr(-CT6T^>g)RqdS`xdQExv$tLf)c>tKSe^i|m%sb^o1?^4bwiu~oapJs%-5vW;*ipwK-^r}O zcNa3z@BNr#5I-Z@1blhL~c%$@ldUzH#_UF0mvA^WreMz+pObgC<9>8Il?w3hIdG zn`LAv-_P>ft9d=0-+O!SZb!S?QAK}+U%8~nXHEI8ort9w$~)VkIr5Ai2NwO=3gvq|MnT(59$hMX@uqT zlFmR)@#V8@ThYAbb8SGb!hYM)nwqh0CFIv(9cSQ)6H zdB;S%4RTAP^H=~WXGuUUnvsCOG(tVEyb*8vpdwkT$TmAAN_`L)U(H{vK@v7AZMUch zdM_0MK|Trodv0>UL327KNI( zM>ISB6bIXPjscw%Yo=ffOLsEjBY}{SAqX{5&NeNw&R|`c&H+CU>Jc~|`dVYsG=2M4 z5FGV(eIQd|US9Nck*Q_}WNHei7#kjrTh-uH(R%dGw@Z$*^V9vc01#a!V%AY#+EeNl zK#2cuk>nZ{3lX5A_`8o`o==Yr>ICa0W_?r1{=r0T|JEl`aeZTi>J{(17kNYHc+F`_ z_OQ(y=T#Z}M5WO+Qa#j8^L6JRF^!{kjH#%7VJy%cM^Vkv{1(#D;`){!7i{Y>abh&g z>#obLEO?(xK8KW4ot@3P(n+AQd*a~^B%_WlWSVOYac zgQCLxZ`XWy6=i18&HcsERrDqNvPyMZ(uP11Q~PaWFP$Fa`XHQjW1UKVxraMtXT35xi=&SK=hEyw*;feMY*16+Wh5XsoaLI@uw{HBq{5Z5m|}1(hwbsZ z<@UR#IGLu!yBQoNa-H68YDSy6Z1q5|(Yal0%j84wHvcvKuQRrSp`ke;$1&_}`iKlr z0%f7*eV*ST^2%zBujEZ*6<=W5z-#F$CzVmlf=3Y1QH=j|^gOGbu*8vq9{USgeE!Oo zODFfL8}p9NSr0>Z_JQ#*$P+UJG#6V5@W9a@&Tu^z1+v&`Qkj>B5$8z!Ped}$WO+^W z@*!3r6S>#F1ipb&zkFB;ywQIlq|rYI8<2O;&G76NDL0a}H#)})AVnTx$-_3=W8!+N zmk7&``}xh2?h|Si>rCpjkg{-nK5qYk3$}Rx(v>C{fW4Ug<`GLQiL$*&!}HN6d$sDo zRE9K9zt>8RyYVEMQdh#*z}g_ITUz18e+7znczO=Q!Q76kwo2Z~bP;nd=NCPW{htsi zz}-xPUbtwJ)Agfa$KF+$?3$YE_2@0#gFr)rS0j96VktSdg!lT?HzqDd1qBA1lH=)^CeHO6cpq8 z_%vTg>4*HLyF2Mc`IMDsN8w8Ni;~`>C$igFYmg-iq!y@1+O3f)d$3czh~Tl649vt( zg(7?3On*Q_1S#9qN?I+d%)IsZu~WimuF&OuajDW4`9~g;!%LJf!3~OR^ngxYnPfNQ z02XL5V%jRGxK?ulb8%kqr(RH|@q^X7-j`V@dX>WYYOv%;T5ss?`c&Ufa@laj7qkzU zGg8(0_4tWiI#7qmV8!TNC;uATO8^?N!5z}H%Lmh8XzuI7y=|SS3@6b)nlRn-zMYHb zap?HQl73c;0@l{~q$is#HXA*}?p*Ixb@JjYBv(SC8Q()mDW@RgeUzqMa1Z+0Of+A9)*=%@hECeb+;R{x% zO8+9L{VDNrMDE6Bo9ayOq@IP4U4b{n`ynoGk{02pKKH2aKjX%5_ z&P>h~!uY&k2$7ecMBJ6!$F3A~i3MsY?DD~rKt2?3NTUF7dvF)mSuu#bn$%5-Wl4*-M40MBdR37Nla4PInLpnkWMoRjEql)vavFWIL>|A8b}WU)^v}P8}~b-7igaJ_t;{^sAWpAhd$) zDpg9uJ1;Voy2sp7D0}wpkAMRtZS=rQ`4TD8DP9q!CtULUwHm|d<6E1sgqnWAhtVI# zPT8Kl%&CpcNr%2^r*wioPt47eMcdJUE?AVP-e0G{XQMh6aI<;0F|4e#Y5Zk#I;cy1 zWw4XvY=-{uejziDiZ+n*moLVlL}A{s zIio7l$;5QaBUtWm4^34(B*5}Kfq0QpTQwuOiqNg@P0d)z72yK%@u2({8~H`UT6SwV zp9U!ivfq`2EXbc9C-MvhJ~l8DJ$&iFY+Q^fwgjwprrD#;+GZ@2+K_aO1=l#Ou=(X9DWa3eE=mmaTL zm1Oj+z%%bMJ>GjHEF?H~x#3yh{lmbm#b!Dy-z69#jG+!Oz^)8mtfW z3Dtq2uC@MF+6;ugi-wVuF-Ln;}9s~jgOs}(2Q+~7h zcIGE*x+svX(}#+(E z_Eql^8xrT~Roe3MCreW(e~6&N3y7?54CP7ahtt{#^6`Mag+T1_{2zMDz%t&*UK65p)itLf8)S%33QjF=OC zyH!EaB(9{*o&Mz>l!;1mRySE0rLU~xj08=jI0^{~FJiyNyt*tr9{R#?+T?0br|!sj z(T5-6INM1|t-UdNl#a*bDcU2<@whYZhWa%lvWS@lue4Jrw#SdG{IbikS=TAth?l;{ zjtPZr&y49<_wX&Y9qV-~4RdnU0x7dvbs5ck#86Cqc`_bUTqzRiPdD;%0?m)~F8Av= z6FPba1KH1SSk<&I52efWwk>hR37=Y=EfvC~2fr)g4Yrw&19udOTn)Dg9-j{;)IH?9 z7}1qhPX>lFRz0>-Can?eYs=$#`@rQL{WO)+D>b+es&Jp?=r2sHuY8Zclm<_|Bcu{^ zr0m2H4g)^+nf+{Sylk^VsBhfVU`{DV)6?c+1^;Y&_JT1|xDc5=dDDxD;Q!(3E5qW9 zl4fxY9^4^70>Og21q&WrCqQs_cb9<>EVzcj-912XcXxO98Sb#VcXz-2mtQ=Y_nbc6 zT~%Ez5*j1833Ze@6X1XD2Q%qhhvOVg{Z7_9a6V}(?B!Xt4|{?ecy)wJhFtimw~AXr zvkUg|uYc$i{>24R(PT$l>}_WdRgnHh)tNxjo)1g#)&DYRz3o!=U6f{F#T!tg^jDe>eZ>I3Nva_wArh!bT!D)?v;c5|@<9X#G|Dt~PZUWX)pAg@vb<1W%$8XxloQ z?XDW5*GeY;<@iymbmI`bjbT*dF4(TaBJcFF?~}iwsj>8wytl>6amObRnT&H}CC?vw zdv%23pFss+r>k54Dlu8t9U*!w8lxZA-wT4&j@EtPh$Yt$DX)ibin$6I;uH20>CKrh z#2Yfi??Dss`eW z(2?es_9HaBIxc%yqA*lRnslNpxWNCT$qRZ-HUDCr-XeGcd6TyNY{SAC?EUQJnBgAs z_!xyJ^j>mrJ#A&)G&Xir{MI0)#X`&3;6V}R;f!7$2rm$BGMlD3tY^#;UK$zO-i+u* z1n!;v@V{F@%mg2k#a-TY9uEm@gFnk<60uBAPvp~+ikMluI=HyRhsKYY0R*t$NV$F> z%xb!5?YZ4aB_Aig6PIwqM}zwD?heA0&Ac0GxHJ;=lw!ulvZi8GjyisZq-eb z=oU1vHVk&XzIti)U-df6V5_Ggk%hQUp#i!$GNgz{cE}JOok@2ps9Et-Ll@spzXq4^)p=kHD@1T?Jdvwg7BpRjYbBh)_ zA^8^2x@(Vx%IZr)?pIS`f#c2ZV(W1zt)Fc`~7UFZyp+ zlPv@?#+T<5zbBI(?0dJD;K=EVp&s(ZFtLp-pZ}pGwfd$b;HQxjh#KgL*PDz&EC1^U zf$Mc>9RJ5g<)t_7K5bdol;O>CYkpOCXlJzC=Pw^Ov+O*Hzsyzn5n7+)u_~R67&Q`0 z|4IOP64ymjnfy4Fdc=Gio5;nQ_bqIttCUK}t;qMg*%ssPOq6Bpx~UQ^t39afA-&TI zdh!qaSwtc@486$=NErWBJp_DtCAYtReU@n#UFTLmwC6rlN9JK3JxjDaU| ziceFQU4d4mr}K#1v}S*CyPC6xvggVczAb3idLeOrazr?lYv)6wx(E53@hFMpi`@4D zd!`VdRopwti*_1ZDc-#hLc!A|r8;R`P57o&ixPoZTQs8Q-iMdnU~i89;C)B3Z1k9B ziA=FzP26XkgTOotlA`4sXW;SMPwASi=7TNF@_^f5{VF_z|x}e_;z&3le?P)v^@#+BT58=XpZ=Qz0^0Wo$^7%9Z&)M?Y8CPOiX| z!IE;W=qc7E!Pa&pbhV;xYEMx1g{0>30z5x@vn5)MR}vI$WR&gPXV5X3I$>@g+FLnn zFAQs(WxII)HeuFcsSUBYdaR_@^UIU-I;2=aez;Ip(}}MHRYHp zaSV1-^H4=lDk>X*L&o91T{WRf5r0wIS44_dG#DLP+98O1J^%k+0HJvMLYusYO{eTt zw2%_PjKb4)b_!RvXUfM>QOM@rrm)Y{&5EM`U{D|wk&FZF#Z!Nd7XZ2>WIEw zu$!Zj&O5=mf5LG4+Tja9XIvXimBnL-1|@17%Ds-%zLeb!2+*>21Vd-R4&J11#Rx@U z3h#$U>piDCoff^)7AaLb%&!w4^f3?r9YP-ipft`L-qu2dbr3!7fYv%5W?qj3v1S{Z zrN+k3FK>(^()xf+h7D!Mw|mZ_Rv=?DwY=u(SMTH6WR>mQ#|8fOvCTaczQ;I`o06-h z)U#oEBf7Do5}7%c>}#;ib4kvE!7csiRC#Ck5Z6}ckVQc+`fknUGNMjM!qCr^{?Wff zk@zhi^322ZTR)=syTH=H%VFq*CHACD&t>wDTx+j)Uyc2F_DSl=QQ4cjZ8vRMR|2Ol zhEPIFyhEM_%I6Ry0p}RCHu(|P*-qD)*xAu|_4ivkp|6RKpW!+23Wv;Ha!Fl8M*yQl zd6Xx1PyYwn@Nk#b5`wbpV}9VR^Ouu8!)DK`a9xH7q1_mNbchL~X<#Y7cQfQ`=Gu_O0-KZp&N!`93M(l>ZUBOiUGNS(n z-&x4;s*l4X8-^YdNtk~o?plgc#9WV4Lr2%?RK#I*)do7x^GjH{)n~M-)LQ4fZoB9n zPlMB^(|C~{i_GQNu=_@VD7-B~hrreZ+Y*+%TYVc*#gD7GNDbFmUWpFOTee-kXfN|=L?(%jUk{cP7GjaE{4yY!Q3yvNVw}kA*6Cz7-<8TKCwff z3E^1ru3k*|c|HC)Yme~}OxKYEHDh}l`glhv!(-sra&%U+h808xte4K#tT3qUJ+zvD zu9@41nry#;dA!w6fw-QGKmlH};v#L)FtU0jqWIpBJ%GHx$=fx7%i92`?|sXYLN5sF zRS(K^ZT0F7yw+3`GkT7siS_um$`ShggGHeu`cSwGAb*5F(=wG*@?)Ik`16yMZ&p%W z>5@3DfU+699T~Lr`gE83Q1(=Pvi@FEH5qg?*By z)0Q!vC(jg-jzI=PM*x$pS&YlOPd2IhCH}nbo)xYKee$9!75Q0&2g-14;I`G7f6c92<{9d@8zl3u#ai!Yo>k?YXTcg&v@JJ#0|=M>$h^hJf-CM? zEDH@B+Yp8HzZr-&(VK3zItU!Y4WL|>I)0DK05VB4F;!oss4ywQRKrVkK(#vt5fLkI z4q4hep4F%QKoH+4gNW4RoIA1mraq4OnS!OFZ(jshE2AkYSrIM45T21XOyu3-xuiqip&b ztJg;gckmEsYmfb|JPt>$HG(`r%NWv#`R9H+=sUhI-Ga`Hv*0Pf4mvR+r5NpYbU9E|L{5l>y ziQ@6PcJl2%$V6jljmzL@xvkvn$NJsZ#8dbji*b70TF*gX8F~Pn5f@)qp&oCKCoj*0 z%m%IVH&}w}*!)me+s2ic)oriSIq$Qwe(2TsrTEi&N2w)WUis zlJgVCvGk#6n>|UN$E_#>Ax;l&LW{r`*%oAeLowyJWvQC&$7MC$ED9zY27B3jeaBh=MFzD3qxa!5Vr77KY zbD;rR=prdy-$ABx&idelctO~*MpX5xFNZv;49B(c)tU|}Wt}$q`Dsk7zs4f*Yik>pUHUhJF+UZ zusl)cu+FESW3?@8G``v^HlftiuI{ zT{e3cKp)iR=KJ@PGc%7UPPH_M3seKF2GkTWZ*s}syG=Ih`6mQviCmJT{GLuHtk}Sx zgiQLTx4|Zmn?*!02vCff&XGRhqbqdYk;*BuI_5)aUm5XmZj{Gc!PywMd5KtA&eTx_sY@@qL|^t`S@Uh$NI99X`} zIhKpg88W<`O@W|HZ-SGyJBXTYOYp6TeeCf4t39Z82nsb)o(QAI_dnjvO3f=ZG$jDkbtQ^e-Z#K(!QIQru$)D^p8ISQt z(ssn0=ntE=N9V9Gsw^eaAu%4!A8DpY=BsbCYK*}4>weRxug6>%b)E9Zv%PwYhxR}| z1j>K$zWVOZ$ItzdVzx)KZVExGPVL4Su+tRrgRDU}^a|iC(tBsH5+B z>0U({Dw(8zqW((3O^axBgEObWo-eGsxFnI$C z?2_dP#x-2s5yr~#6n}rvnf6*{bplk0HD=ArocXn#%>W#1zziKSdF=04u+V+hJk=Cb zmo|do1-@OQr^WfMigfCc9*MA5&5e?}T6(}Y;k^sFZI_FAdwnd($M{beEc`V>ctTYt z4eQ2zFWcDx(Sr_oYCbk+S=$|}!3O73KeF7&T*xX{!Pe@?yJM29w|ee> zy0iV)Gzk@SQU1f2idPcwutHZOvGy*t9^b5Z_ z=R3Bt>^6A}m!IA5%fYD6Q$tvAXIJwWrLFp#6t?-tYmz{V?^u_GA-~7>k=uClNgpit zrbky{V$SOWUP9)2C$;B%*M-nNpu9yWKt@Q?H$~F9dNIuj**y|k81&h>8QVWy>rjdn zxf*!)_jn3~wc-xeCou-;*XQu8y0_W1$_lYVI^i)vXTEzc=J)437GW8ap<9U>k1XVl z<0?Ac-)y-lh7|f7^?es^IG0LmRH5Ur8}C{5KYv>=Nd}!h$6l%`i3~8{{}$o6v^%zG zSnhj4aswH52qsZHb_bDw7fbBu9)5t2)|Qn(ncSsG4i|ru@}W+%x;vA+B1S!FTxmsg z?|~Pp=m$SRRLa=zp}zwS4ly85RE#Q2)I8dbpFK09a^-gdPwTO3kyY?DMp(67?~B}V z2?W@>S~TyE3A6Z? z!aUSU;ZbricxGrZ2Iwb+a?;xqE-$UdVrSLRmQSYB=o(7ygO7baFGrdTE_xTyZW;}WO~?I#vd_PC{qfDH|*h0iknb|^k;%tn_wrf zSaOuAH;e$c6LGl`l2DCvoMvGHuB0vqtNyA}wMRCdR}h>veN%vE{R;(`ab$#z-9DPD zc{VZo=f%+%5d{A_q1P1nG3vJ#?BQxV5Zl3p>uo)tMhO)bY23cSeMAfL#W)lYJ*!) z&)etbK|82ZxcrL8(TrlaxAhJI=z07t@phjlz*Os7YeOM+1jz6lQ9%a#jxaik$IZhM zQ37T#hT8XySuIJ^Dp4#jd;L^}Gej+vFs1jNxsknnCgeG;S0!~~C>{g5H{wBqG=p2G z91|5;uhgLDv>J&e3$8o{Q(6nHZinu&v(f#loBZ*q8{0+z7Ukzc`VJu z+a>~8)Mfz1SgiA8F|lkawg?nGzmpTr?l<6lw~iN#4EMug?g!tq{e-DG;mB zWj|oQ&=^xTx2$W3s1XRi{}gsxk$@;KoE$YtH2 zZc#6e(&mSY)l9(Icp-Y5VUmYg8=|X^4_Ad2Vpi(yo)F>e+QEM8R^!FOm@5zoHkJcs zrC=7|7xvnnbJU0m57TdWz#00@GhtKSmK9|fqRUwoc$8*`02F4{&HfG`l)#o6s?P=X z0W}6)aSQTF6ogBpR=B*g_uu7U^xq~)5{ED3?`x+Ri>-cl8fx&^W zop29Xe!FHCQ&AM;om~&_?Vp5os)*?_uH|$Wkxs|vHroY)oZ>Z9p<7ZC-U^GZtbNGU zM#)CoSoO6hKT}vH5{dD=c5+!a_EjG2Y>^;n4VCXv?xF!2z^Ye3y&5U>+K zI#iOP`ImM6i7GsK*U+z`@oZzl`HXTZ-p&T{iRB+}bp}=CnG#mEx~8^*!dS=p$L6h3 zU1vf2bwTIF4|^&dzTo|by{w$9lLpWrLmj*Cgd{;8)M8P|xb1RK&iOHgVWOm>U0lGr z0llpKagi}Od6I}S`+bn?=XI#Vao$ccATCcn8=X}+`LzAX)y6+rkzpVRm4{ZThUN6%A~?7E~x5OYzQ%$d3~@9dwxu4H1)0x#rhn z`mZ#D5HL=(=4>Qr=RV;3+t%y>EZNuM^J#%A9<1K*vLkLx`bl=WzlYb-NE-o8GCH^c zlWiaE2I8Bg!~=^%S!>eBvfHprl7$Gno)kfn!2PYarbs5LHpx%HTh~IPPi1GzkHv;efk@sp^?XhOP}^Z9yLeU*(l?S` zlKDmjW4j7>d#Wh8csUxo__1OzILeI|bvo1`;HQ*zg4HfSZf~wZckiJE)h!|HPPH|b z-m|=#3!t5)r+n0ar|Uq9@|k?NI~$(C>gz?#M|#r{UV*8Z0I0aevGggV@>0S#x36z)9RRh!=pfM^J6XiO;acRU2*tj^|q z>=bPHI{)|CVk5?e=KXls9^72$ftX8)rfwp8pMB>lFDyzrjNV1kztcM&oUQxz80h|JiT1 zJ%8Uc_?M|kUO!@)V@7aMl;ubc_rj#460Sl6=a_6ukW+EInb%Sib-((v#NHlEeKXrF z{JBn8YScSlYLqrbf39`0vDQ-w&kc3AE}LNksee>5*-i-d{Pph9q&!JV4ln+u87>;T z_vq-tI9@OCcR5i1ZXI|4x+F!5g?Z4>F)IyPHRn|oPNj}7m*6DApx6n-ayHB6(lL$almqQe zIF|IiF=kBPaIovfms3LNubE&0%qjW`^*noJ<|z{A99w2K)a~@8P%EqPW|SyRE4hlg zhs9}hu0p9w$W8FLIJ|JFL^%de^KQB`29lx~A<{3V~XmGJHq-!ytL zLO=1hTV`|nEm(hZLkjgj?2*U)#qkl`jHPZq)^miL0kdV=lk`f8WfzW-)`4pCe;+F` zRX|&R*mpkJ3w$M+lN{=`E5|%5QbJBmY;eMZxC| zv|UtW75Jr<@#w@xAEwqa^_)A_p+5q+xcu-=ht~LLkcgUG?ZA^`VV+GNkm%AaB28Xa zgWXrkp%}yeQBq;x8gD3)mJByF%KS~K4xV;El@*!PlVL@h{9Uwnyi_d3in;#Vf8LY- zdYKe)pwW>*qkjCFaU#UO{rS^@<~xIQ(;0>6r(n=aAeNm`bPHc`8A z(piuhXF@pth^sd9L!jmh`Mv+?Ac?@Ctmg|QvX@*gaWYxdryn!KMqBT+xFhpIrg5Gt zsd2kV1t@x}bC;ugV^0;c+BQ-fkw#@}ckkB6V`yY}nzsp%6Viz~n$n44ADs)xK!Xya zZ%M_Hnp0#TyG-UbDZPUGK>1KEr}g*KbH-O;ysIiBK4cMLOjX8)>poH`SqAi&-XTa{ z#=G0Ae{{`LdU1SRny#!C{Ih^~SBX`sk7bLKZg~!91ERh{e?+8}hd8#hzFZ~5hL$7CA1EL*u2nZeVdtAz?9t&H(w2-~TSQvI1Z zN-%rj+bp%!xK1Hyc6z66aQ(0*#vFZz`fD4KOCr$MaAB1HJ3ELxU+#9-PtfizY>w2;IGK13UAn zlu5Q25Zg9&p%vAkZp!Pieeb;!iSzVfN&`(KSmjccXwN>$wBxFT4Zn zfVMJh@UlU?7ser~tFT7A9uL*CFJW5yOAqzN^nE+}`G;H|A0yIUeE&BwU%auxirajs za_ku*N?*F9!8n~$>*<)6z>6XUf$~|RwNdK4K0)S@Um-w}me_=vD2L7Pvo}v1WB2_? zx~IioX5PS%hM6>EwDU79+siZh`12LBD0G<(iqGwW86G(ZukUoFWvUCES)8@~`b=>+ zN>B^r9C+vx5Hz=1)+u((OD$#lga+)Wl}&1 z?!6yyH4^;v0A^d@MSlX7$~6ppTMl%c_>+2cn>DaTBUdws6ur|;G1c9Qn+Y5*+&8~& zP^pqj0pYEM6OsOz0?TjzA;ZmNpjxBc7dF3XhhC974l9fOgjqdwL)c5#bqWQ`0Fuob z-VA$nYzfy-T^O2!m|vxaN^(Z&DXq%6xy0hlBu~NC8J(~RLfGV^`UV|joh$8)G1%=K zi><2;Ym+-TCl1DO$WG?6CtxcJ6gq6&)TUDY5BVx_s79abg?r$+OdaO`l^BTXJhs7( zM~p|)h7_c;(_uH^l(l-Com+(fNA4Gbl+iat99`X?tbDfWzVXAv8@Tx#j0*! z?nu+3{AFrIGM7d`w)t1~&zdab?Zg|)Rt!@D>lxlY_yAiqC@8t1`u%^tJ6@D1^gy(> zBLUEBJx&nN?$q3Se;9F6E|$5n6VDYMlOZbHd9;HZaEBRy1rhX*En9Nl=2-W6w!ne^0MvpFGD%|h#Ta^j0eD1MX&sW(9rGahK$6`2a zHxfcDhLv>>Ru`W;Z=A^ao*o6$+pHSrpx)Z0RwCJi2Jtt#|NVNl@1w{o0WVt4>amz> zhLaCM2qBVCWXU?$c4W~CBNt!unVY$}fAgJEmb=q^+V!>SOa9VXqqfpzR$=zvqW{XU z&=>hzE)ELt!rQ3TpjDsl3FJ!2biN9MF^88teU?SnVn+W{-8c>VE#e|jd~0!`@~0?$ z1vW*`Y%--4}ES&>35YCF({+qCyGE8<#nr|!_6G)<^M2HNqOC;}R1FDWbpa>9i zZVyGodzOhYczoLeTer#T&8u6ad20aOzm(;Ya9mafA{@<>z3mv~HMsJuIYM7!`h8TY zLol0`H`@_HiN-0S#Mp)K5u@I9e72KVmt#FV(5A7s3ZqjhNx{DmsWMbOW2055!{2-| z^pP-@rMJF!G2j23P9(C{dNl>z(3}&XBy3VBT*h5iAjE(Sd86Y7|9F|){>5ob$X^(I ztt#|mufh+Ex8ktJd;VGn*1@3F>t+1N(RZVO?DnQga`tB+&1HD}k6+Uq@q^Kq^ zXNgTk+J&I4!Qb#(x9Erje7jruuH6a60Lh6nhXQ^mjE zgL@-dHX*KFfbW3|=aRM4xUL-#y61kaRgOn0lkH+C2Q=|hjwSvkjm=Py+^;L{dapw; zea0^?WXQ(}QOB~#glI0#8QOPv$ANTqHU86+b#jJ7tN*jPpptH1!u`*0Ka%R#ffZtm zd4_aC@w-{J8+MOpr|&}&*eWi6i-#?41~Z|0(iJzKwPX@&E#tDLW(`j2E{IHDG# zZ0!9U1pcj(LDRb&^cvCpVl%@eL>&b=WmpcPY2KC|pD^pAKil9lsYH3S{96V>FNxyr8Gw=XiklE7hkW! z3sd~Lst)3LH4MoYqiuX_}cjoU<#*tUjJB=K1vqk zyEYS93xd|O08Ikf|0#Glfv+i}z)tByITah2mhH|>DSs8}A7HE4Ot=Cwo-&~L+Cc=w zM~crp!UCk%gSrmjN#VF2{s<#f8%p@v;UE1(z+3M9{5L?m0rp}eK5SA;n9rcxpe7cS zSp%G^YM0@Mhytvl?;NMG!l;r048-8XG?u^NdkcI0RwA;iF(3*J=nTW4P`3Vo;+FzV z9;(9Z6t_AZYaRYR;&HHufo7H~(T&`1TwTARRgx;sY7gI_`NrL(2EV&4jcc@}=09{- zFVwQH()`w@J8kytLv(@VE3CE3y0TyGJF$u=y5eg5(Twuy@zFc`iGpS10;2Ku>%Z?a z>qod3Cg25hks2&>?Kq7mWnVP0#Ea(!pXc65gL&28q->Afl(DrnROf9Ke#!M*J81A1Ony9s_T1NqrvT#K|3)6ChBv z_eJ;0)^F~Vw#$Ga!uuDydx6DzDhvhrWMs#2U(OH45vkJqIah)j#Ty_Xt>F(Hz8kyU zh~Dl1Ua9T(0$Ck`0u!X#C z>{(oMGF)PGzKLb?x&$tP^yPfrJ)*?&lp;I>lq{8!ENGK_i>>XI zxK_U`aan$az?K9k8>N^XKL(SU&vx$)5E0$TA7RlvD3v?Vu5A)t={Gu%KRx;#tf-DU z|IDXg=~?C7qU|=sgPJ$ybN2de9j)aX$50HWnEd<<)$a0xpZ*fw+s7xF6P5;5 zJnp)Khgn!pUlhv(s6YP1+IIBN5y4sg5SYU}zqq^$XWk&i- z9hsWtXVtgkLePE?1C~leI>DOTYE=;_H|9Kzw*zfJG}(seQX#qhyz;szOL>U^q3iLv z0jF=R7w5$x6COr7UJ8Gng|T235v5uGmSwM^hnX4AV?6Oa2ClrS`Kfq)>LODyG!}^1 z))j!;{Y4Jn4evi^x(D3Nk?wJ-oY8C_Vk`WDzE7=f>&agh#?aG!J`RVWdiE4Ah6z%~ z)V#f2og~AWM`Ql~7rc`Y7fLGj-V;BK$jliyrLA_T9dkKG{K$uD6p z$UZ=OyGRv01=dxcnN9I+kNfh|4H6i5Wd2yV<(!u;J=5MS|BHEk>l8bcBU(6 zLQMv4FhLT*P4(*L!rO=@6}{dw;rc_d&c6sr^jAW&Eus}w0z5*E{1(1CX*LR4dm$Zy z_P*Jk4{i4iJ9s7Rp@v9ly%*j1@tb^8gM$MP4V^A6Tg)83I>JPS$DBLRrJp2UeEB^1 zv9-tqevUp%J!%%63#s&NxlA+ATQOH`G?2D&;qw&3Z+Exd6XkO|B z!w{>l^9i^=zJI!fv?`71Jx)u95Tmr!%dc|pT*-*Wh zz8795S}QxdFU=_XL`dmTD;)nmApj=3yi^&vN1fmG^CFSRH_V0!oMrZJ($l}6gfo>$ zx1K^j34M&u;=zM?Bkn}le(wN0`4#>I_ug*o>o4LU%jeFc80<~SzCt%KxHDs<_5r{f z-t=N8m&(<@_Mv4Y3o~th+CK7|wG?A`cz(b6H|2SbD6t+jO|(`ct#mlmCq!;B zJkNd&g<=m@NI1>m7Y$qVUA%jtyagC*S+ylfQ_nTXci`p#lxG25A@y}if~v!8&_ab{ zgfl*q;s!c7BF?p7es{WJVJN)fyXvtwN9Oo7QI96Mx60u;WNUvSi*q-ggb<1?38B7( z_z5Z5wF!N)Vq!43fo@`o5k4fW-Tpar-WPLGyRaKHQ?DOm-5IG2p%N>P6?xDS5s+=i z@3-r{prTE62srrwq z8OD<_Ih3Qf^zDmp3%)~G@>W0m_-qyAXEX&%d)0}cyK5C`a%5i3e)-Mz%F*cW)O;Oz zsHk;B7l$GjM|?vffIb0l!T)2`$3Yk_%U~zq%mmD-3Z)r5)9>im(-&RKO`61@zR0n zD4$qa8|euHwgF;27*hA4cpn9omR^8l?v}R&0LWx3bVF$>W`9 zB_;QH;cYU4SkK*l?h`WWM6(&@QZS-Ocso^#L5W>NvJq<>kd?mo}R`#Vr$L(&S&-2y}zZkF`9V z`bU}bj?PHk*VvDyLXoYrB|zg57vRSm<6m?f6VjA^Zx7oci%S-v#E%E>dE6Sa{zTN)aQ>i*C1BrD-#lSM})S)Cr15+SX8mW|EA4eTjy$;S2&F+ zolZ+^-47YoA`;-R^miqA2c8-5co_PQq3>m<+D|~JW3+l&gY)XAE+571>MY-2FA@UV zm0sG#93Znn#`PJ(Dd9j4oOwDeH~rNY+dcM zx^z1{7g7@{=UGS9(WlcJx5#Mzae2!fI(@VoBIVZrai&~<+A(+{FezLX4I`3n7S zxU>5{Uh(SKfoICT^vOAF-BTk|7R9P?cW_OFQTmFSrMr9*dOnxn$|^_OcB=`(;{Tb} z*D=xo+UQZsRcfqyp5KavE|MoM1@0(}JWVF}jKfNgI}VloxF1TVxStVn9&G|$Q5$$i zCU>xzohNbcRC1T~4zV`SkUQKvSXQsWdQDKYfYs8<$X)TwGsH^hPG9F&F`%8eZR(lL zHr{@US!V8*B5$A+a4@TvtLo0=4_)Z|fN~dR=*7hBx#NDzNvcCPsLWCZX0B9>l zBpvdc9A?Gc?d!k@#b-3{TQ=A^-GjJ-Rv+uT^A3c+#6JNbj|?QYL&jEFDRJgIXTDjk zs}`~LKZ&>P+PDIJ*{_K_Pan*vz7%p!QhE)u>6rhKER@E&(^h%&ouXmYmG(tYKVhD* z;NM72Jl<=y!3rUruDh>xGPzYU*(z;bSx^zBL?#6q_LTvh?BiS8H>S_VA{eH_Cwqo$ zDM%D)fspg519eb|cNj!Z2A`tO%TLk2SDPsHV4rjSZ1*iAM_fih-rRTbW5aG20{^nf zNL(ILEYmMekv%m57Ux#jb3t0rh@?l|73>7Ulo9VCP>U+2Y-X&(Urkf$sgrs%#9(U) zZ)Go&m@xE2O;<>OJ^By8E2hYVtmSi-59$eV=YYEJ6;^`SPIumKF)nZ%}BH1GgFq|m>HN!Ufe)e z%H#`hfJ|UrkQSA@JqvXD;%3E12$d(q9to8+L`-MX`|FY(f*ld-uC3!M3d&J+q_FxI zo4@*#Jd<8(IpBg+Ca~9{>R2$}aXL{mswo_egrXh+Grp}|@NWeh`do^x0k|+3&2{Vw zr>9~r>V4V6qIdbXJ|7B0v7U_}EQ=mnucw}o+ajLEUC#ojA)&xYqd?geo*Pk*G^LM> zF4h7sS)s#_!kw~`@b>yYs_+X{`jX^h>Wsgj-XWJg{f0@8y)136388{SjpuR-B%DON z{fj=z-+3{@`JDaI%7cafK5S^Pb*f3fviUCka$SL)*Rh?rC7Ahi?INN)uFl-m6P?N2 z{!dlstk1TM5C-F>-EGw^uQ3Q$-?^1}a9D?Tb>Cl!Ic1hm#r@+4cqd%}ywA%x+;6bQ zmOS`|e_%xf=^Cwb8^p=(&#Nd+N-I$keD)!9U{H@t^BtZOH4PGVue8SNq7~Pm!feED z;_zKAqheW^Q>NU+{}KZ0~~L(2Z~%4FY}S20oXgV1Jk@p6eI zL)_9sQ5Wj>tpS68tDsI@T8bmzU!yJUF298@yB-hQwZr$2Svr=?F);Z$bOaeh!3rK! z(iCpp#`^V{wh-Y#HN!KK{fuPJKDl`Bf1>{X)HB>0QJqVgM`!ueFB~j8vCK?mY?m*) zPjSi8p83PVA9P(+Oq06qWjrI!A|4QGIpRJk@GSoZ1h(C8LM2B^ZnCzghav{h6;fc( zNAwOZI!`_epT3L+Tdzg;%>=`I_Jsgc%;UA0lQ)4}NW7=5;ti zx6@l%MmMydoSVB*N=K6^dY#ooK~zFVI*zM{rY58_J9D4;@t3Yxd81AnuLzC`{3`9C zETAUjJvDpI)Pvj^vagFB<43)Jw}13_P#R9y$;i98gNPJGmpjHue}45#(KG46yYs%c z-$34+K_2;B@Spe5>QLv$s^24UlFssf$IwAH$zy{#x`F~%I%y4(0el=>4UsXprkAGA zi=j4FKM+E=g3GJY7_u8Zj0&)xWLg>(7uc6E6`&@5l^C%ybE&;(KW}U{jTJuteT!NH zYIb#?YiJWixkV-EO4S+gMn8OlaK+tS-LXahYU%&P1l@p!)*&%@Dp zvtcYM&-PTXmX^qDjY(h{Xu3Xoh$_q+yz{<<^pI=_GKxfTo=#TZLXt*WR3hET=>I3h?462q!-34OK+3=Ov zN^n_o!*IRmN&??O!kChcbF}*}X_xT$7uatl7h|n4X^NqN01p3#8?s1OP@VYwA_{8G5HT)tt`xa+0{rdTWR27j)XR=jwT{ zcf0u?_8$HKF^RlKhcsGcbmgCKaCmi z-{mJLopE(wLU?I$CUheVIlb%n{dC$9MIW6dT2fX|4?ag0(a?SP>2^e&w!igf!}~s` zZxW5EG{4ka+v|zEVSGvn9G?U*lze<>NB%&u$@9Hs~vd%k?9t^XaQio`Pz)|&asor+Tt{0*cI$xuW;d`Etp$n5eI z)hn2y&)DE=`=WC5|F}BKs3`aT>r)aUf+Erkh=d@ebccXSi-dG{cMc%ZEz%82NOun% z(%n6D4;{nIb8*gnpZoluSF;vt@dD<$ezEsw?{AWMfVqQG#Ssa)aRlh0yb1JMZy^Q! z#Kvs^hUj~Ry<56YBZm_2#-Il?bhm)v=GnO-IP!jDBK1QJsIfNQpFA(_rjT@n|W zS#d#;@1Uy$*?eoh8hO6{urDFtDW9yJxBiOrsKbW7znM=xZV&O@(-!b|` z60N2_Jt-EhYqrhjm#SR!1+mpnb2ldGqf`vZ0Bg(m>*u6Ge0yb?;p#tzlN4C1&aHKg z=i7Rs$?-qXznz<{nQN8ge;?|xjR~S`cNp#D0J}6qh-dJrz0ZkawCW=5NN%9-<^3{h zRMMZT0Qji-duAktV%P`U74G5w#I&jHeY+(s%lbl{&e5{5hMU({5F%vrCR|HRT^FKt zB~YU;Tts2F5diPAmyXJ{X{zl^c_REZ8i#W;AYpqE>hpo{s=#YdjSiyEuoVK+w5=Ah zu39zG2;3JqsMsU$*jjY`UNNO(NpMhcn$8q^mzQzas5!%9-yozuEtj!f2g}`~hls#e zcdLeVZVUF$r*(7oZ0eE`$3a-;sKUmGO!O#fz7_Ej!aypnFX`L`ZGHcrPAQsjPt0k5 zEhHhvK-4~I{Qwgc6Xxw#vDI>QxKv8rwwFYBxvO^XGtTVXhlL#tgi62s_aH>rN$GiY+@F0UIjygs9m#M z&;y?-NnAJbUmB-z*82d&Ll%xkYC3Rgm_wp~F{=g*4HdQpR}T1jsT;HjjGvcoESKA~oP%rjq#XEf9t+rfxEEqzvEzBi#u|XZS3sAL zMR@JRT)*T7br!z12uAL&i=XX`2eCqqEvyfT|Cne#l9wkJLCg?Vqv|GSOl$h!ZE=Q_ z)`Vz5+!pM&ip?hSm8$b=m%K{39ivv+WL(#jFAGnFH@EfAB1#*T&khPU2t9RO7LD07 zEafigHbs!vyp&$~mVMIt=vYG?K4$i$$mE4I4hn_6k}a}6N!!1cDmk~;&1VVQ2@(p^ zpM>GZ=t7ISQ;a3|@~xVVeU9*~-az88830Iq4IL8RGtkgOJctunC6?X=Nn+69W8%&E zy3E+95&aiX$Jr$>X_P>UiX$=&deS8CoY()`1yJ+Qf%J95_S(z5l@|DkjE9BS`2?b~ zj{TqSDe*mqfuYxB*)Zd6?c^PQEs~4lgAoJs;rLE2@r=voaTR!@mU!24#Uy-o24I+? zAL>6Yu!8f6((JsD6wONN|6O@nj!BA?QtBG6Onl7ErXRp=Gffy!r-EKW+lONjQA|PM zo@_1wrMZ0pxaX+t2a1ws9TW{x){^^Q^!Lh(#6EplocQ>a9KRi-NUYHwJDN;`qYqU@ z#xlC02GDLQ9Mmq2DGZ$_Q2}={#&rSPq0E+em$V*#hmnb$a&3uAWgt*a&x|qC)c}|Ri zZNepnYpUdayMS*tRVn9HIORTxF`MtKpIg$PB=rKkX_2x-dbX0sJ=CQTXXzS2F8o#M z&=uPmni4!emE3(Ra#o$NvlL@{l7*Ht{|o14A$@Q&#d`48MYAwsziYqY*5&MC-u3}V zfTYh8q^q&XEcVjV)}ij}KK>sr09CWG5ox$M6BXoByokoc(@C2y8W__Sx;{pmRsJEA zURYY6O0^bv?LhS3K+fL^knCTo@I+6O(Q9nk1Z=0BpKeJsK8OzKR#zQFG{U7TOdoI+ zM6h^zf`)^^1n#EEw$ZNTUD+kBVC-;sWJ1IbYWu*+rX``P>48EtP+*7+?v>jSvMcS1 zQNuhOKd-xxO5o8GPpX_;+Dpf`cU}iZIc!sH68g0E z;Tz5M`;PU@H~r7$>I|(2WSb@g7()>9WV<7HoV6T`WaJX=w008i*0!L<@^+mT zyC3IY(iA4;L!pJ7JpvScmj7(DxuzJo)7@7>mrGyB6E`|t>+i1-!Z?VcZcQ47zWyDB z!jcM&DJjTLe&8hJDWfki>KY=@o9J#5{@!mqjh0(=-)e`?dKWxHVkbaFjK-=l#Yj*W$g<<}^fv_J! z=T-V=d&|8bte**-=gUBJiwVAX@Ica0= z)|^_hS%iP?RMYa`+I`8_BMpp(Qw z!!A&1K6}Mastzt-JD5O@KTm`ROkr4|<8ntWmG!?x z;3@WdLpcB2i(H}e&Z`omXgucHs9(2r4a40p*h;LcxB?H8{g;Pu-K$oIkMce|d?&$w zJP6pQNsHYCSl1PwPpe_3DOOs~dXq-j3tzBu%I58oco{{fC>cbXCNq4mE&X7Wq(3$|KyR1~3Ac zsAj{wcz6~O?c@C9FX+%|&Y}q{)g$s-YO)2|BPkngDS<1~g6_gn<{ufqD6oCr`}1fN zxlUk+^z>ZW2XuVG4L0Cq>Kb^>1(xeD$`%I47Wd6}mI5$w{AkYDt-1GWB4AOX$sj`K zi7+Lie3`!zaIz9&rXt*gguRQVs>R!*i78AI*k+1Tvk_tV)`Dv_mBNOG$bYq=RIUhN zDk7Yl=El@nbX}^tKe+9qqw481Pp|WGfL8;aZ{!Ku6vk{6csCx_Xdkd+rl?+QJ{CA5`JP*_Kd8UjA;P<%L^X+IbpzeYO96 zymBDS5J4?U#>-DdrB5ctLQY8YC>-i#X|KQb3?$;n8*#hxis+-V*D$JKs@QR?pGo5G zS*6dcQ00F-||WZSUWB z%-b1(;6Qk9th-^UyZ_HPbqZ}gn;}60e$OYs?CHSSc3pN9NOLR^Fc)wo@4TpxDWNrI zO^}hdC%c|{@BR9u>2yG0j5BI)-%&mXNQ}T8eg35WISTjvv5=0lfJd4QFrM_p2LooeaIkq1ikO zddhPqdz4k!*ZwHxs>9j>hh@x2;kh&&kL%?sVx$;GNk~0g>+v!YL3~hIDj-G)6FmUY z+zUodvl}Ju%iOgJeZb&(x0U`03uU2Qi)_aM4hC%xSoLo(9MFc_H>cb{ae16Ksa=;Z_B!)z*fzjmydfh@lmRve3L! z#K#>aDYcFgHHxkh&`Aqy?F_c_JXCaR?XKe;Li>?eMI1H1BCQ&0cj3|GL zr?mX;Kc3Rs`t$YKNVqa{3|_BGeDPZ1xAOZpI2OoZ(dtQm#y|yvoVJrEzwJ<-O9T1% zb}K>F*Cn>xYvKgNkNqwsX(J5{H*^-0KOcWMl(tkTwf;*LrJ_zaizWnxoFtuMB%jt5%rbUY_s=4RgvB2zXTigg(l6OmagT^F5s?7e5-rR zeF1H~@O(J4@ycX>lhCTDE?F_6NbJ~x3d4~~Z*6c&ijX30pduglGm8Tzy_Pcx5a(?8 z?xqsN1VZBtFs`&O65~?Uf{f@jzcS7`m%0)xE&{nc8aKq!o}P6h5~&gMD*n^i=7H-s znYkF2hB~i0)8ML!zGX02rN^8vT`bci1YThgbvC3mkIe&AUfCkveGWaFWJ^Y#j?4Xm z_Emv{4}`*-c?w~k{mk7U?huCV{@9avU9jXJ{Vm1(X)LL9B*kc=_c^Fk(sLhTJJP>% zg1kH0<2)UBJFew8IA}SPMkZo0;aPPa9TD67w6T^BRmXjUuHm#qb0;f^>SvTNg0b_p z|36~H-!Q;1KEUZAul04`5M$)xVk~@sp}P^cS-&%#io2BAAep!JtIEa8lEw)y zeUE3bp5SKxb%hb}gc#c@!12`)+)ehdZT*EP9MXqP^F zx_|L&krnu6c(y%C+iS#bv!HGr*>ZM*yR(y*A?u{Ae_Xd|5A7cH*ehMy_E_Re7z45n zrGwYxHt<+I*Flxq5!ttc6jv_(Ey%l4r}a@Rtjbr5B+C=;zynGWJLJpitP`U)$8A_g zdCo!fmWBj*TiEuIrY%I_=<@Bbu;eYMtoBB>r2C3qu%!7$ zRVc{{$cnq;*~~tU%+$3UbT*sT)`}3DF=2PR<1%|)s#|Ze)svawJ=K+@JW9V!+;)QD z4~Qa-a-G?Krv~qJ{7zZ|GH9chfuKXf9rGe@hl?6vfQ)!^%b^2Z=nzRu#@30W!Nx2R zxw&=D44koP7H5YOl6!i)4DNV5N`UJ1Hcnf-7MV9Yt9V)OIH7>6!mj}wA3lTJCoq~% zv}*S&_k@3|QH$)KP7E>HuCARjKV{+~#0`Cp;8LM0HI5Zj=}Fa}r0;+RG8;GJ#d4H} z&GD+A|LD%sp-|{42FX5ZoMN|DN!Mhszx%v8eCpDi>^zK9INrnmjzUfOOgf@~>XQP) z4PrJaB~dlJGtOOB%}maf4;4gwy;?0uLY@AqJkhg^%sdV9Z1>=hpAA?0=w< zW@*zv?h|TseqJAEqwJY@H#+D@lw-}E7?6v?uQSuU1leC`Hp70T^2Ri`4WfWywfeVXoC>F zOP*|*tzqROL}Uyu>~|R||6=xSJDtKC!Gj-;=8)^>ilfU{1b#VDLeDII<)wOYvp{CP zhsctc|MWR_ZOIK|<6|?=EhT+^pQrv;oWJ#%b zf714&RLihVjW@g0U>mb0>n^5HXKWm*|K|3gM8UL`uF6!`dB#ZB;w`P>3I{fe@a65J@5*x6N*9f7F=Skf13*-XX#1Wta zM7#I-(NnzC{y}kAG)`|TVN%Uv#(O}>Sa+ZCQOZy8NNmw3#cE*wiO8dC6`!gB{M@0l=Ybg zK|U?Dh%)c5x)PS}Ut5VmsPM*5cZ;wvk<)^=5`045pUUoDaCY|}3EYC}@P$spJvEex z+*6;jG8EfZ{o()8GzDsUVlB{_CI!C3QyV2x)A{}_hOMuLyn>LWcA@sYyl$z^w2mvC zw)6Qf6?dYrFB8$K@? zfmysT$@GJKTHAN9(>9Wj94Ce=i$)mh^kJtfm$c6SDq=-hqu|{kGsSOC=d;xFVX~j? zdaUleeuD`b+;%jgI?MSspVy(Hzt2^6F<8s~jNKbA7GJHLu*c7LE><6?7CHWilDj4m zDv4dk|1vXK*EsLL!_2kEpDWhzd}8y|79{*69(D7_n7Tbvsa;z*Rx!FNMOz?>yxtcS zwDE5m9q(dH?IgarIL%(=Mq}DPnw@G*ni^0ac>o_f-||USqF0u!J-@;2uZDod`LuOi z6RD?aPiS=BJ=|y)Mqs>4nPXgu$5DSfgyV(sAo!d{XGqn^8ZXswu!!F5~`2kW^~?cmE>@@9ju-=lkrUGV#&(Vruq9>aww#FiYj8!;*U9yVMAr-XK-%_0!sB zasS8!tZJUBL0GdXl5DUB&Xy{$|Ae4v9bZg*j$Kj3OYBRVG|y)%?1&?7vkK=h_CKa1 zIhA-wCX=^T#NkL^g$N(uJhI$;mGA&rhNnI15TzqKQ3+~_W_NYQ88zo?jO7O1<0r+Y*#mPuQdiNX1|YZ> z^>JU{O+c&K8U_dJgi51>gCGH0)j!MGI6m6Ar6K%@G}U_7_p#YCKxk(&*baOt@S!HT8h~@-aQIt>2*nKXoph3xPQ}OaeGp zN#2SK)JLs!xR3)}$Wr?&f@e;=f6*c(;yt3*@KcHWVgDe{l3cULxjflYA>ToA{lP`b zc6->665%|FPy0yD_G%F5cf;6&EnP8mtO1FWmqBghoopYAZO21kby0G<6;t2c+q~e< z{2Z^%(HyiLcCO)P-}amZ%h{SeFLUp~GY!+UwyJ?K-N*Vvf_v)@4ql=;|Ah$k99|Fr zhF9Jx=VmnCtJ!HeMoq(x-=j|+a)jU7YTG%Xes9Y6R}mJ;R;!d;;_~Igs$EtsreEdL z3(4wnYh{Ciawi7jZvUjcAY4ki308 zn!RQ^`x1QiUV4gV(x@H-B%dj$`7trFg!{UPzd$4}pWz!3r&*QrL_%1F*enS+rrY`1 zv|)E7Wf>J^+$^L?e*RgJvgU9DQ58L za;B^|sm!~8hkL~vx4I7G`?orqcai9o^{-6FmXE4bpV_>kA5HSBEV-V>ps}b0za3fF zO3P~E>~86afa#muI8x5;IuPao%|28S+X-4;a=h=?E(C0Haajd5+Zl*2I^QuWyec|m zZolg7h??$r^KIFsad5l1R$5#oNB4G@$1H*inD}#jaoIl30P;Swb?Q{YnzRc0Kl?dw z+wY?V0I)p4ZaN7uy#|R)@TaER_f%v{cj0Jk77)SJJ3QnQ9rGW1-B>Z8c>fDd)3bc8ES%Lc8m%y{5k3fyY6a8uykFy-lh2++4H@B@Rh`5#N8EPwS z)HBSaLH{~21{Za#e6f5UY!?c-9{4Bu$Z2wAoqyn1U`b;a;_hxapTf5ON*sMatbXrr zx-6I%V%&o7ZbqG_7fLFK5qE-jPRol=g-H0^wSeO6y$qyR>SN)vEyY)HCOZoWhCv{Y z*TrS+A1&UNM*yX#EhuS@Q*>ldd>=R^9;J)ycg@5NNIe>*dnXQ4X*x;Esl4Zi!jz7J zbBa|Bt2Z3h!(RgFEm*0Ko%LI-vo|zvKA+BSkbAcLf%fW?466hKV$55~a_+5C-upKSXV(g(2pL!Q5+ z1(u7(TW_g(?5XNN$!E}-N~7Aoi0@MGp6?;(!gELmga%)&Ob}T>%QB41k1&(J4U*Dh zy3^OeG8>dqF7w|@pv0Tv${bj3(%Uj1%sDr$M02Xv;*GyZK$qFSah+J?ShWrw_`lFj zcc~sN_t50ObX5+%s1wm3rf+|?v6W}_=oGVnGcl#LnD8e;?vhZIMi}?0Z!rK|DbChO zd4FhfflzK%ny9SI|5Eg(^PTDs4OPFE6`T!7xO}@su&5WhLrXPPX^9ebQSTJIYj$rY zP-=DnH8+HCAPD_@?pOa%D=&9&j-;l)8B-4Hv?);h%nG||3FlQY!uy%*7QidmiWf5X zJZB|wPTiin`K{d^TSGhEcWmNP^b*VO)07OYXNsyH=Sz8t@dCQhlpKO4uoQhBjq)fg^KuLRx;I?|I5Z>d50ieSqX&!xi4GOdmC|uj0oNb99ISRVCn|Kbof;-Bx z@&Y>0YZg4v$+#>&2h-lNIOs=VKD=CSCr>)a^$Z05x%s9Lo`C6I?*Yuy zS-c7F*@6#8M0&O`M0gITwVy@&3V{qEojtVwLt#)JRmOUiyGh=_ z7^efi?s~@-Zfz%W{X=)e76gx*vl1|3&`3}iijSSc=QRU)-0UzAE)Q}q@~3JnJB7Nf zjkKi#z}Bdjz^61aX?7W?m~)Q|fIp zZ!@2E9y4?Vx#b2rt#GntHN7JFEEst-*%%}&Ce=$ZDIrc{By3st4k&`MvT+FL6R9mu zt2_V2<{c22Zugt}cG(U(=>#km77qZM@?s8ZS~X{J`0_Fcc}9?S9G3H)bWnJH-w7tE z<{0a)5U&70skioD>2~Aj9j?X4!Q8@nEfVG%acZM(gpLNl5^~?}AW^gUqx%HJJR_41 zruKP64VzxJD~?8Hwu1fu12dqgT+#_`+wIB^?O$`h@snX8TT`Ht{b4Bi&skiu~D0( zu}mb!bp5l(GbegVg*QeJe4fuBk1o$IlrP6$s}L+gff9kJ-)0S;2T7wRFFS{q8Rr_0 zRccp?SOj{)YMOk1BLLe9BAMqneB* z=K?&QxH_yEjyo`-v-j%RKPuJDMjSc(t2!<#zAv{C`x$1Z(O5X_IO+6~d};9p%m4y?x9H@eq{NGJyVDP_zIjG`6h`20@RLm|GhLF-qS~h#Rsr!z^o+5O-60L7Nsr>n?91yfSCwG0A0a7*I23NW`HrA5&afTu$=&L_K~_jCUpAQeZ2=BM1J?0R`@v)XV2+ zDO^Hc{nk0)d}C{Cjl`c|mRx;p(1Vp7G-%&G`kVB0;q+{%yrYQ7`A7tJ(0vUyQEaOl z7q#M#MZc|&#f@LcRD}1YfT25wL&u<`zMH>lzQohgjgP=!3#hO;UbPK$TD~5-+|ERg zR73#tp=jL2jn6gAvI2Ihl!wOAan{X)P9`Mk*LPxxQvrYOL!S;b-SgQm=zt6M$=&3Ve#Fpj3o=^yN{WZ^aC;0-Dd4A&G+YmLz z?yDuDP^cT7e;ejQ@`3G6|65pBX(jj+W49}>C+`A5_@?olkK~KCu<`v5QJQArE8@m}h@UsUWmPHEt$Q{ftofK;AI$a?EM$8QUQ24U1$F z)*r~{005zIH>Bl#(Tc=AA2)i+YE8i-AoDd(E0Y9csesg0GJP_#y(8=#?50eov@|EY z4nyyBomkxR(qaf`enwzW{?M>k!ae3E9`YC8lD@jm{{;79TJ~n1Zd}M%lP97+;l{)y z+i^%J;t;}+E)3aY^&Hi(EGeut0H)tMO)u7t1G_N~8ONI34U@={+d=izq;hAMXz~{D zE)Lp#zxo>s_0~O)i=7#6IezW2owYm$G!e;&=vcE{Q2x{n4_RKpn;KzbnXC(A`YntAua1B|KnV#7;Kon2)d>a5SyMiDy4Bq<@fMZEKl0JtyPoS?2am2|D$mB)9 zG;KJ(anHKR$kANsxZcH`Q6qulcB>Rwpo$~N<>PrR z>E^2_dhoG7XdLa74ZAXgpF=100LKUzkJE`9v;}1bJlA?hKC_kPDm&`piX)3j~;aHr#aZmbcH+}6)%zQp$t{11#0%A$ls-0k%BFYXwh+`<8+NBpu< zrvwBuhmjYx;QTQ^aX9cHJvtrn#W9ojv6+!qi(ZoMCp$M2^S zk~<4EIpe>CcNbnD-XJ~#E{QA6T#vL#h)OO6-&?+0IB{Nj7NUTpm=uf)CNsUpl! zh7B{dPud=vt}$Mn&9`3W2cc6jY-CdgNsTH=({dt+{~9YV3Q=|xUek?pHDUOr1%Tem zH|t7wFn~ETna?w2cI}`k3_rS+1z4*pZ1`P*d}1Z6`BfEP1$;5f3TYyzi2bFGGh0=*(3{aSTI`$}&0ROXgJ}U3=dcxLRvF$SLiLMp zD>Y129ZUy+htAg-Nr%Y@$n9Y^Ag_=^7M9rf)O9#|GS;JEl~doF9nUCwh)+)<1! z4tgqVrFu3z>R0(EU-_5A!XEdR%0hbl9Y}WC?N8Ve z6zXL^5PVY;*63g9Rw&2?Y-IXW2r>C0-0_Xh@AHyq)Q8X>h! z4Z7C?Ls~b(2K%K9nqry`=!)vm_69TY9gdF3pgFW>WyBG&^`d1#l!E%Y3DQR-{ST^Z zSWD*FZLPQ77$72ZPZR3?i+>H(H9h|ya>6e6g8gy0eh>H}AWB=M#HHrDoiG&~l*JxDS;Fl|Mp78sN=9m>^57& zF;lW9tGT=_#6fOiaLRqqLPW#=V(xV7%P&rtPcJVUP#`kfeFYTf?7dPsoH(|^Cem?( z@i~$6$j!@;Wt2ira*ydUwxyyF-?-6}`S|=ux8ep1`cR!u4}(3Zi!1t~T1|FN z;csDpMmw+@TK11n6&or#N3#5Z^V(JYg7&VZCq96Llbm%cnri^p7+sz_n2@ahkH8o6 z=@Z(BWvTV5h4+E*aR2>&{f0E9`Ct>c<9Ivg-IL58)_q|~1E9}HY!}t7nSSZy0V|@v zFVanX*Bt8`Wmk7zDuQM8^h{8yLC{*gmI6gY-tVR^AH~OQ^(qm|OWZJvo?Y{O7Nxd} zRCik_hnR|HK)J2J8CZjj9Csd=o%kR51;5yMBQqzFJ}9KVW)``y%@TN7_-Xm%gM)rWG0$7bNynC~rhXXe&? z{W%i&wTV|v%@UvY4VSya4q1kMkYwLWmxPV$*nq7(>p?(#EksMB)i zsCY$8AA?zGGvS!^!0e&}Kij6+Q`*~@^FPD|$0v{|%)~y}R2FXM0I06p#Hl_H@F>`$ zDinmT@%gu-GsGliUPE4{*}?yKRCp4YVEQ2XK^=rwnL2To$`>2najFS|&UUWg1}M<+ zH{MsY{f`*&x5bi6fcA?6{18>M{W(T114g{MF*QqhAIcp`sdGPr9%MISACXh;_5FD> zqyc|QKDzImu-1$E@?vQrBmj0IUFsdMbPB{+47`xG6t*4ijan4CsMtLnTRSAJS8b2u z{7rY^)QYx07y|g)-Zg%b#q^bJr<}}?q(_Z zJ9ZS?#`Jx>hi7&m?I}JvAF228N`%=IqVnWO;_xTK=pUSYhrSynX^{BI`}N()l$LFQ zv*?^1a$Tg!8iULCt*Y^$(GDL6AEBEx+frF{II;C4FCSv4(dHM2;Tqwu&16fP@0i#3 z(x-5e7qK=cFt{>Q{@nMaLpRK<~EG z*dK<^+GuQ!v3Ue2%&_a>z0$EA)6{noHA`X{;?&NiEvp(K!-!kT-(CWOFArm+DM0Bc zzSQ5hz6j4E$~rC;rXHP%Ppf-dlT{2xzD)7D_%8DNdhrM>k(bK-S`e>ox0bRZ7P0^N zsJck*OAfKFLpm25^dIA2RuyBc+pU(vyoRAMS_X{tV_*z(Fvy9-aR^kJ2e02b)5a0% zNlXS>lXjAs;0}0V@lbt>(8>@w8l}ebQ(u1JGdQqry)hll)huT9IV>X{ovZy1_@@Ag zH0P+$F4W3NHB0N#+t1P}ZKk_J7X1YSriVF1T`7UAcokF6VecW1e=#KrM^MzHQBRv7 z3emL6R2I`C)ZY(#G@U=W=DYx5jr}<}4b&sG=>Oy_7j54ck_sX#h@CgLD;rIlDYmU0 zg5H$Ombczrr8o~7PwcA)I=zaB|8w)m9-6wc^>UO=ql#qqcCx*wq{3bHfvT3P!3W?3 zV_7j_DqaKRYg!-Rnh3VuHhmZud=zQ%bq6*6n`xNY;-qs>1^*dT>yTB-Q;m9!F~L2A z$mEBT(I96G(Ywmcm|8X8nZAaU4YkYKKxgZx(v#Nvg*0{@4$dmp0wK$Z5TzKS9gb63 z&UrtPob=Pv*aMHwqZEc?LTr({EoIK#R9yG^?xQ{HiQT&%>-LlM2|}pjEo;TJ1E4sZ zP$gJywyd;Q?f@$AW5;A59sz&Z1HJlo8NiHpLg-d{EYxss4M>6c4G+J2XQ#Q&`DgU; zSZ2q2r<$g_17#bHmkA_9|4Ur{b-&gN{Y(Gl?W7YE)0>mh`=f$2wzfc0n&5S^-cNY9 zLMC!8UH{gOoOnn~H|2mU-^%SLO!5V6qtlI#k4lYAm;-{WScq0EU^r$U*!9+CjnfG1 z_F}{R&8|eOxQF{O0dnI$&nJ z!rnJ#hBsevS!HRZBQt#jVP*$m#q#nTwe^8kwXd4*xJBX%{Bg_Y(>9lCj~=~@SVq4x zM|pbtCoetoUV`<1^v=Npejmq2df$nhP0iEpM9x#`iNIB-`M<&a+5*XuKdY5+i&Ntv zycuI$_vw<5Jwwvvp7-*+wyw?&y2%E;pck*J*WYlWi)?Djd-@=Bho=tBV@p?>0M&M+ za#(oL=*1^uIJicF~&_ZJOcri{k*37eeBSS zu-8&y$Dj}4lsxfN-e*s4e33I*|N2*q{fFz!61wdY(UNuO!;IfwrvJgRuJ){@G;W6H z%~Td{WZU56Dw(FYQ0}9j0q$|G5yfUdG3D~IRO&f*oVc{DsNX@EOh!r15#c%9zh9-> zp!pJW(WXN=Xl3+gQPYYpSz?Fd1KQ&@p8=eE{+g-X!#zBA3&@Ky#25e1_oNg#YVzMU zt?gt!`DdXbu_CIr%!nrBQrGbKonRS=JbPbs76$=r{;8wWYn|tz6)B7s-FgXDm)^t&KU9Hw7Y={?|}sIe4V$Y^&?{y&nze@+pY_ooPpOf3gsmV^?(@Q;BP(<6t% z(bF4iD?ckxSEN6_IQm7r>|_Hi8s> zaT+EG2}tp{_kcXEJ>JEP==>ye_@DI9kpqsoTdy zRF?@vknp9}EbE8;{_||r7EnzGXFu4PXD|q#UU9P-e>)L%%3bHZ$oiD%bphDMVFUZV3}e3zVJ_im4q zH)VH}nJsUhlBni-ul@bT3qFzUX9*NvM2gLky;CNgz4;`EK25M3TF4_y`^BGx?9+QI zzQdmE!XsPzJ2TQ$6cHUihd1la%ksaLOI7~PoiaA#onP>`{q()UOp6$~kBGIedb5$2 zy|7YJ>l!QP*6(ipecEcA`X>8EyqBcw7Mt!cZ~SM!r-6L9^n&>R_h>&vQvtL;FQS-s zAUY*YV8_sfzk+{yg|W{~Wpp!%X+IkbOs`8buGbA{a*5*mWt`cuP}n*6Hg+%i6v@ z*$nGy>~_)t%23u#ZC|;ZH6?!;SXP>C?Ix0@N!1YWw!qhPPH;YapmcbU<@Q?b6^f%ZrrEFL+^Ts z6|Y`%BUz4+#bL0)TD;3_(O_t~RE!B=Xk7rS-sWJQ()2(wycb$uXen z_#<;4$sA6>|AL1-)@sShGyG)T>IC8f4OUdpl`d@JW#qzbNC%&!z^e=ieKMQ-1Y)(n zcP49;rw^BVT}c_lt3G&ZCh*#A{^sit#y9JD)it%51{qG03npN=@yV$G2Si6R=Kt5T zhp~a~t)1(`xH()j!jON85Jc=Ov9I3^sF|_57H!>O>5SIK>@iqJK=B015?JzAdM=Q4 zheX0c2q=MQGSqYE#CT)Fzp2q}1p!hg{dNP0b9PTcAA2N}5?`hl)rDI=L!ORjj3$<| zPWhvOX^KS{3@a@Bw3|+Ws@vP?A)Aa5TO5CCGw5k}LL-JFPVF>mF zI;(Qq2H6sal2sT-^{KJpm2bN!cino$MH{9=7Uug&Pudw^mG-dH2GbI(iE^;Ji2c02 zlFTQ5AwmY1^xCWjjo_Bbt%5jooT_BpE^Q4TSTVa91sOQyTWzs!@+)$+)D#aqBYM zwEc1G(7I99zLKLcp(&m9BbnhhbT_#9E0No#7|$>jm1;=iXSWJ}A2v36xW}KecA%VV zc{DWcmNfo8${TjyI-5W8e{6>4kQ0c@M%ou^>)zS=o{&3pF2bGKc7ife%FEc|$y3He zcU`3mqPx9cvQM61cXrI{IY zPu&u!xXn2;GrlxEkxxT}xJFBU{qozkqCpW)HB#5-vxc3l=UstAlU5Zt|LtarB8p$d zutXQJNPfykLz9IIEux|~XBd^tE9YAu9n<_we03WGq{&-3a;+yO$){qZQ&LXHeECtE zj(fd9?nv*3Gk0{p;~Hj7PXocvHB-;nAIPFAxBmU1NB^wMANDpY+ddKPlI8(CcFpyS z=#)pkL7es$f0u4fDA;c452fw**0VU?6Q>z5l6JzVys!YcB4uPo%Q>il1#ztXb;bpE zYYF*XQWU?Yl-Iky(VMU1#GUFgDr^DL2va5s!x$n zzB{nqTPO!Qe)yr*8P{bUZkOm)r8$=W+POjTer0;hONrtc&9e}BL)gu$1MwKt?U6RLy2Q+V< z%dQOCsrPb=1{+oaPD}J8Zr~)KDrORK{ zTM5&T)eiHCTNYTfW)LN(gEEjqdGK=MI`J?^FMqD@E4_z_Xn%by^boE1!-80JRw4`1 zNrnYJRw;Fn%Qg+aW=ZD{vFOFG-lyqRk&eoPY{mwwk4e^H$?RW zuajAL0s|}}4|}6}Z9s!PFgG>uM${7pwK6_amS=a&og8&8A|#ur4wG|DK#pJjiG|}^ z7lM)SP^n^N&d437^+^+1be7R>|GCvbl*n*x!_L*amHFeEm*AwpW8%G(lNIXMecRRr zC`gx5s3Pqe$B^cLuV%v;fuEVsizW-b&25mZ7t$$RiYDz)zUS&D`mwd`&!Ifuxkpr0 zXybq;0E{qvf*+{w-aQ{=R|f4~TjALcwyk;hbeiK_(ZyEg9q&~H3Y=G5F8C5!Wiu{- z`u-Jl(i2AOR6n$+Z4*K~c(XpbJCm#Nwz0wx0mFGj0d9<>u+YHSZfnQFzW4EFrzVI2Or6U@;WG z*#*xXUVK6GS3(>J6Hf!LQmakQf4<54$DE|tmogm05{D}1cMCzAKY-K=9QQV_d)5pD zw#=MJMOW#AjoMM&tT(A=#G>H?&=!Whc-o3`(3IfU>;%zXg>dBP^>oMXru8{C9|f{k zXsd2o&``e$-&qIdC$L!DKgGd;*wS(Cd~rGBeHXjde`Tv(-IsO|a8~7Uae8@)4{H21 zZ&B{Ec4MER1rVL~)};Krn-Z`~INbVcO64p+kFBRL#lY8XHUzXAj&W7(dgXt2xgl}v zpZx$u!lFs|THR zm^!kfhcWuSoiq%Z4frJt6l+2feZ>1Q>ZiL64iqa0YJ`V}0bJu!%)_Bc`>)5g3=Q}B zYdfGCJnyz^Qp9y{3N~!nDC>6{iRQRDU@vpA)T8Y<$hMGNeGL*^QZv!Vg7TCIbRl&# zDvUsvW!@Lg0!MIyTC3SNt{ZV8P9k?eIK0x*3i@g0H^)EUf?tFj>RqSX>`@LHgXK6X z=Kvn;O>V@ieb86lNK{|i8a=Y{lOf_apZdwoE!3?pxv;~o%EG2mYUIA-yN^EoVJ@xB zj|@h^OK|^AL+peR5@;q~v8P{<^SIwPDaR*zhscKbB>#-Mp#Q@jp8l7@b>G(z%HW)D zqzV*yyagX%v?%V1m0JbOK`pf`XGvNnX|*v04r+l?ic#ik9t;D-tK|}1EArO`Y24N z$~OOVdKOY=&h2^Yaxf#id=#3IHqUh^Z@aIq#V0{^+Zc5e2{sSRZ!Ce%XVk4hW{J;= z@VG3kaon0#t3|ogzhItxKJ3e^BU?0|^0(5%q}&6Kowqm;-wi$PmLRv3`)7H-U*&W2 zfEOSm9sJz-;ezS2XbBkG%=~n}0G?Ao;02CZldv~Y03}M4{MEqYyyG3e<;0PY&O(}e z2~pVE8sdT5`?LLq7&gS6ax5q1d zmodmD&0L4GapL-Vr$g_-x${H!Bb|5{)!kXZVYIXj6F}D3#@%)%9Q}dEKKh;4ZTlf8 zpW0KQFasMY%8M=XbEho|b_rcUKLhcKioUZ=g}R{#Tj|+%sF}fidwvP+KhlTtt`!j8 zhW0?+MsLHZS>65mm7D<;{MLcprzl0>!2e2(O&cm-+X1Vin@UEzG=Cs`4I6I26519! z98mC{@Lp}ll^WitALbjQyr^oihf8+q?kyhFE*}cRW0V(-mm7>YqkxsdQ^HRI0cTsl z)pe_yH`%jhY(~2G>|O3V3a>(Ls>AqYJaB~Y;N}WKTHtu_Wn)4kbQxBfHiNeTY^LNN z&HD`>Cc90m{I?6>KU>6KA0wYyLE*Y9I$r?R)C_rakRIy*9_HDXreUh45uO*4S!&an ziL%@TJu+!Ex{i6Q--OSAUxh>Np(kMx-Z;NTO@rz#K;>RhlmEOiw5fFHHV~Y;&f1fY z<3bvAa|g**@P)V5Y6|6Wix-j6hfHF4wl`=eM=N>+hhz=D*Jyn-LUp$2>+(*2(jAu= zzq)ZOk1qx@gxkFRt8Rz*aw+-xhdpe?LDl%J=@4Dyi7?|BovM&r^O77t@^M0yFmlY}lE zqzckRKa^saUu0QRLwD7lm3lsie{f#lMG4t#H*OrXC&^IQW}33A80O2V@nhq_zc%;E_dvr zl1HCfDybr(=D`B}HBCC26quegJ)nSF%4Betd;E^cuO8gRf430~7>-QQseKf1m1 z2U&w+oUV=)uv_c6X#NNH`~!c&okeV@;N>?9Df7tC+>|`nhz{7dwli`k9OXO7FdK{h z)gs9vQYM?jXbVV}OPiu9Stt&#_(BEBtFwfSxTi3&$wuf~ip1~uEuDA33Gm<(bG02F zQ0J)(PF0oW!dfjb``DfbMh9@LP5oK#5IVx}cCN#TEJ7bpclW+mPK7>dm0a3aruC zlTu-+GzUhsgFfK6fNySbO z&hK-X53tPUumo#U zQRoFPKu{Rhk~udKfB{4RL4QAGFaG3_TW|eqZN}YaW4nRG1y%8Hvf3Wal0D42lFu~C zY27X@As+4g+v*y<+qK!`WZR_ny*J(0X}+ro04Pm^271}%#KJ`{+HW`CoAeuFYOc5$ z301v7Gqg+2`Lz{2Ts-t}frI6S5^+*|))nliIxsK1UEE>J%(R0-NzKtCS-?X7V>a}4 z97SnQit<8!vOHteokJk`%aZD$ipxnwuZL#JmrS#l@+hxoq@+~$*38T`pFFR26%|C4 z1}JzqE^dcj!07;v?dlU%zJ`utryqE2rx3!o$)po07K>tYsEb}!hQAZ!)nfM(s*t>N zwNUh9&-=FwWGamjX|@sPhbNo+VzG9%iRPdy$#qlx-WVU*s(r0O&(|i-eaSQqg|TU) zA!n>!o!MDYPdh%DM;djtwuKj+1kGQLr^#D%s=>MZ~OY+2sel9PSKxCLV6{sn`L&EHSo5o2ylJ@ z0dCrj>J`coH!LVN_6$)DdLJJA<{k{Lkc&wx^ z2m%Gsp5>_n%dzXVGp#|#6lWJ4(U#Bb;z54xz?(WrE1q}HP&`wk%RF7IKG)~uFE85~MAL`c`7k45t z;XIPtc}IrUrxa2p4(#&>Evuajjt;(t5+e6o;f8^C@N5>-Eh7p`-TD4Q9LL9JdkfpY zDi^MR&}-N-zUL=+GtvtVOevjYS>c=yY@&3g#T0tV>d_Y;vSouKLqpQlmi?E)49}a6 zl>#tJ7pz}#nsY6J7pMCRbjhJKCm6Qa9ayfMD-3s*r=-j-c{rQ!Z)rlzJDubfq-yM- z*;Ps;yNYyyLMmoy;-#*DExC)4Cp8o_Pvb7pNyUD>n^)w&#*Z&yQrOvX z71}PL@ySo3+x4_nU8N@nxqW5^B#dqQ&6<_laz7#aj+|#2*pbS}#_EPvNSTAAN|K`e zuP<*o$=x%4p&g~b=Cw{`P;Raq%p4^fB}utM&$+zs8=&-iZY_+Y4DGRy#BpJg)5n~s$W&TJLJ1|~N7!a{Fzs;>c7 z)eg6-KN2dJs{8=wIiOd#IBoY8|Aj^Me0<-2ThBt!XfXP>qqu(xa4WP6xRNIZqqMf> z$0ORS-bSJ}4P(1s2^%dhlgsS9=7~T2O!q`6?b!v;0gBli&RH=X&gl9%gonGxI zq~aw-dwEYYAFGfBmLxApLrz+qto*TDq-^qyk0G+BiZwk0(wvGeKy~#^HB8Goa=Ubo z;1c0jXG%Pj*&Nm_Wp;G{Qbi%4fG{^?7`?f3cl1ISy$iw!E8!sV3{dr(dB4BSyY2iu z&meG|{hr~5125FohjHw?&G1Ns6h1iMXapKQXl7y)dFvpDoWU0mHr%$gM7IN5;zG?F zYIEyUF^)jtmH@A8>kDzOeBag2$Q8!<(S<pB-?LH4X!x5DSq`rP z17C&C7Nqa3)eYYW(W?u|pSY*JIJ25ByvUf`X3TO68^ZMU!3|q+s)+tZZj`u>TDZm{ zS@L(zo=+woual|K%6_+~-3@JiXtQs{wBI8XY{lp@i#Exlvr5B7AtiLO8{$+ocKEu} zug+n`Rf}0yBNpNP16Yw8p-d`63@u$t1u_ml_TGqfS9WVt+_`bJ5%oKtrks0ktnAro zd7RGgmE$qZ}*AaAS)$0kHksj|CJ5}Avg9ohOO8M$L zddxeb-zW=f`TL5?yp`tIDSed3OC5{Ilz-4jQ|lNZ^(_iFMjf_;zA6uYBu4P%VZU(# zeWfE0ge4%njatJ^@S8!g9gJrU%a(wWcXCWVHuD?arQ*HeevTrc8&bFj6X?@LHzaetZ>y~4PSKPkMOT9jEi(nYqpMDfWIOOLMp$%nYIL8leT`Re%S_|^mtdcOGx^r!Bv#W23uXrzT!Hf% zkOT!)hs>h@6-=0bZAyYyjWYJV;TwY70A$=!n?SQnz_b%2Z!V{(?Ae6gEado?U;Kl! zdBC)oNE97R3`HaXq`TFg%nF3)Q(Nu1u|r*nKUWX4v}jP=Kt62ddmJ zXi+rKJ~2D6V<}g6+=Srczp&x!Q7Fqi`<%+MiN-H$k964Ok}&A?J7<&fxqLHgx}?$8 zCFVXn{-#z>$>8o+j-U)>KRKzF${|F8wHZP^a%yaP!>Qyer{CyDHA;|KErhGNegc0! zGGqMwT6D3Bq_&y5KG(ss`FfAfeBTpK*ax0?$EUB1(FwhY)78D_#_RVWLcW#A8~9t; z(|fi4q@9$xyf*0pQ>5mczXpiapD|rLaCjq8bO|gGR&b6@;EZz8dS%gMYsthvt?MSz z+z&TVt1jR0y~U*R0)uIPp#P>v5s>ghX2r;e;r7D^7A-c)3%`wxzQr_?w$?1t&>y%4 z^p|i~-&b;Lg)r!NNE~kQ{~}lV9yi+N-N9<>jAtSRTD=N}|9+MKnZhe9IxbL+^1ElSN_;<+NA2_=%oVorRTF%e?vGElTHTWdQF7{8A|zJ%5U>wJ^{;VA&>HLs?X)k zw{D)`A0`%3zr+((jqYReWGyc;m2J7`7ZESLyN5f|5SDA+8-16 z)th3lYza$*+WB=*U;*PCug`Cp05R^?LZrQ6LSaqB9cIvhoqXncz=53;H%ev-tBgKX z)HAMix;thNeb}E12?WJSzOcW2vSa=3j9w`z0nbEY)vFM6S4Xsnf|ThfUNYA%|0W|_ zZl4t27>XHwHi+_nM^>g$-I)-uFo{2^Y5SGzY^D~3_e0vvSS#yaJGX0S_!&SPIaaMq zh!}15Q41?O*lXJt+%m#J2T4ztE>0DYrIH|0RsKcZ-X-^@g#|=;*n7{Ng<)%yKmdMX z=M}wS(4j4|R2v$9@wxsd_eB%vCX@@z92D68mf^J4?CX#_af8w-DAP)D5d3O8Z927?U9^_%U&l-+jto5lC~vd?K$p-3qqHsN zTw{6pG_$Dho@U2<+_v`D44W^JvFDw(z#>jL{@TRK`Lxw=-kH-KiZB-mZqpF4gcvx) zps_$SLZuHJvU-w&)~)wwJ^4P&@x%02CH+m)SCLdsfon9s$I_4c&OI{sXN=7xUzm<$ zT@5npC^$NqODwr>i;)tU-ke_8NDE)8nS)DHpyo>eg1goB5W7#j)={5k)>FXbY%nJ~5 z@d{mMx%nufK>{*h8G2UHtlxjqh&kWg$+BL5zUW(Eu3K%%nN1d|bFoamRfKCq9()vg z#Xd@XTXvDEhlv|-s4~~(&zL!;q_%udXI5j+xram^#HVh1f$b3F9y0WVLa!a z0jnW~X__bn0F0fZX9)o}8ukW+vgXJQV(_ZP^fhR#v9?UVn zd8YP*)X#Qzo~6+D$CVLdf9Pug=4j#QyCAqrW~ROd`E^w0BRH#ZYzu9QSOmFHvzby7 zE$^+W_2-kKCzM>Dr48Kc89sZob5~^PJmKdtS^TIiO<^Fwu326vO!SdydKh7GX3%sot#`b zz2kdQQ$15rSW4v#DcE24m5dE>Rp@XL&3Eg|JJj?}Kc2Q2euzX_x6Cp0&s@*#`&(8N zr6oRMpFcTBmvug@>3pjh;H%59a1WAG$#!NnTzZ5y2N@3#3}aA5!3VnOR)$}{mb=bb?!YRy zJaO^QL~Xrw_q*mt(oScZFv8uu=b!W!jAO;G&P#$iraR8yCZKpZ-l)y;-uZCM{o^Fh z@3HVRn!_*qnkT6EapLC9^7XI9_}i?{<;+A9MH8u3IkebjDkT$DC05=UX|{O|^1P^2 zlu0$ZDUozHVM)g!#@}FnrCL*{74wA^`9XKzw&ahNqCp$F-l^_qK8hw}X3R0I`3J{i z)1f|tjh{}s0@ZSWhmXp~i!njS0BGABH~x#@`XI8JqC7u1X5J2cK1M)80$P+;@&{V{ zaD{F(Gj|9VEyy4OhD=EZI?as=F#H)xrE;!;#+m~+fhU!&q)pQ-v&*zxs3y?9mFJrA zTeP5KXeS|!!rpm+!tX=f^L!4_;9VK(C}Uw-n68BoEnH`9ps$W$o2oNG5hgL0TCTLt z802J&AzwS&(DU7yzg-m zOt&MaCpMHKeN$gW>F2z0dTzdAXXHs1-j+Q=X+Fc4kN97lBu-5%k7Higj7+=JXnom8BVsNYjf(HLrFGMy zRXIi}w{{?mRMZjq3S?{dd#roUhB%?cX>&wGEqw4syS-@$9gMi73=3Zbp@d0x)uew> z_T(sG@?=)newc4DEW!to_wx34uhq;=;0a5ZR^`n}$JQ3}-OX!Ap9u5zIU(d)i2@PV ztT49uSXvWSL<9Ok$;wV`H@T|_WA)~(Ei4d+z2JajY_>A+P{lfQtAKH~)18$k?TgdeGsTYIlh1e>hh~1K2M$eC+47hEe^HVvbaFpa_g~jpP=pRW>>>( z_!vapbhcsM?4y1}dFUoUg;Wn%yo-ch>{R9(uAR1@y3o3@Q}XEUTv#t1`#=5`*LFC> z+p55e{=L{sj1N;DpWOc|-2YFQsC2R%P{G-nO(kftE$xQUc+thIb$!9clE!z6e2)3+ zxK9j^(io01B5QKg!@;~uWs&v*oRkqYE_t{IDz=Z$=HW*CMC}`IPpGAMHSuyj_vNMU zRIC>IQp6E~&i84@HK$UZvzvp{KC{PgN{FW*2psB>X4#icDV|EXJE0in?&H1(2N7lo zYVq)ihZ7@}B`@?mio?c< ziTWW3*&CJLD=hw1xIZA?j^Xc!h+`#6X3V+{q-CA`m7Q0WSfFdE2Ok3^(xC|No~6c~ z^)?{aNT(N=P^e7@hG=N>z3Uq#we(N1jbQ1=$RfWC1e*vaq_4ehfyacvp0O?RsR2>3 z@RR`JXr?%<6t~A_R&5)?21{oM(LC3b6z2IFJ?|HCK6*NF5>B`8pN)T=8A79~dQlX% zsSQ!y5;@+)qiNAg>Yo^~bIhx;ERcN+iX!M`OmJbpWC--K!pHoNM`FKSZ-p&7eITp% zx*l_gWvy<+2|R{Jm>g{RD0jx<-W&xzVv^|;mR+GFb|Q0;-&gXusf6p+>;IX5@edUy zctIN-NA#N@2WUgx2->dCFw+fv)It1NOcw8TeqBs8W z`fvOTepaN!ZY@G=6bPgeasMuoDxZz@CvzozinD2Yvq>+pIGdFf@wIKRl=M~|jT>h} z=!3;S0^=E7HjfMYP{wm*-_BX!mD<>sJflk zr^RNXQj0)ZohGWxa*P*ksbbfT?Ms_))X5@(tt3%RH6^lcQuXkxVnXR&=13OV(Mj2KKzjDUJJt0xky~mc zU6L1=C#GDM)do>J6yi3{3E_*yv1eNG8uYgRAv+|&mcNQuO>c6XIF*KBHY(kD0 z-U&iZ6C$moHcp+X#63rYRvlK)2PJ2k1H#TF0;X)~7K&4Tvm8|6n~wJVm#}9pE0#?- z(WXP8ko%!RWBAw0g?yu~V73A)X-;4w`G&CaMHv!A*iTpxDbKe!^KuGAOzgmh ztx(-Rpqm9DZKjY_IS=##I#qbGC+LoQ2K1nh@TT}{S?48Vn;A=gpFh~Es!w^}!oc;Q z1&M6i=lPK&cTvI%TH;1$%`mPya@OHbm78~JvA;4VZksjEid!^(U+|mBH;HfJ`|A-S zH32jB^L`5qS9XwAt@;S_;JnGWe}mss3ml`%&JoCKz86x@!M40$7e?}9Kd(5q>X~!u zPpo#o2`Ps)S9#!P~!;SryozkKY6*oDLMCs1ZRei&g2{x zZ&W2kcbjn##T9a^oh-I7UQ^oY$w=+rStXHXdBRo!N-09L|C2~qhT%~z{ z>OlTsytqAaCi3Q>0wV+*0Twcc2k>mq&JR~?RCq4~FX#%^e<~fR{J~9QqvLYOo7%hx%$QQm$W~_$ z<>py=CvkeR6UxHk2@`CG|HM$&Ex_aDhi z@3R%3Og^F-T^__KkuWsA;}N&H#LgZS*unos#VLfNEJr=YSlC15iNLp%RsqstZ5Fex zXN$&Xk5h$i6!LuFYJBNLmqVIMe%FTEkCv*2RsF@4L@$@gV9gf|r6id#r*VE3qr5ss ztM4WI9_hz7k$hPDpr6T>d@5xQeHxcYGbCG5Lp1Y>Ggn_O`J!d?T=J!yB?Th+l7{o1 z{!5+z+V}?W9xWu*8} zU+52~Wmu83q92}QM5;yvU1Qw%euiHISmn^cqvaWRrQ;WJKB-`u1M2T^)$9XOc9J*f z7R6#Jr?wQj@F2|t7~jcZGDToS-d+E>U_cYWfXZuTp?>u_dc^gD7h&zULVtK1$T2qT zx_J09V2gBqIMum8(=c(r%|O|K4$Y^;H98Zb9+Hh6Cu-J$=-kak%=^AqZL5ST|9xnu zI_a@p?ZSjR#FJU+py?-mpbuizyYU-MP=86_;88CGZN=zkh)ED`;k7&?NACfwuh+xw zZdTF3rp*EHn)bt32EYfV#LRj2vC(?1c;3TS!s$D3S=y2NlAWR9#dbY|6IqLWk!qmVLGvX)>C6i z(M3l-L8G3T5D7c$aw+6#Kmbm{Wj0*(v=665gBDp*uBQQ}6m@m3 zNRBjZ-=|fd{S`$q{U_tVUH)X8RD@=)3`5ZTmFQ=i;ub1X9#i7g{<5n|`VZdoR2*Jm zP@wHPkL_?71kZ1pn}Mb{Kx3Rciq-Spzn2FN!AmpmSEXVX<=a3}Q_S1+Q4N9`c@1P= z$+_%)Gb!Zl=nmy{oqq`m81dpO^k?Q()5a$a0oW$K&euuks83VriNqFKfmSH4XSG;0yPH}G*Jo}{dPk3Ms_MFtmdXODy{k*kgw(gEd~MM^);)Y zn`Fh`DkrobL4`HvI;yK`AF1t61YfbL5Uwdty_Fn!X|al~w)asCbprRH?IoJ&1v)RY z%|*}r@~#RPu@5aq&Ea2b_#b0s&KEL&Dd5n=%Oa6FuUmYvt3BzfX9;ys+!dcQ9^DwH za)YQhs)=Jo_D{F&6eed&L&u(RyQN4&(pSmUQvK)B?wqq;fGrvP41;GIRIYRs9G=Xy zv^`%C!7`bKjq@fK-f>J;-rS{I$TG)h?gcEu3rTn*XXQr)L042?qT@4Psjmc8!rm_6i?KZnpjTZEJyh{zff4Pu4ZqqpboJkm zE&s&^z#Znvb7^KLDNU8s>vKIIi_D@4+!+C&Zj#{m^W79pgP376S6;p`3>@T>Cy*Hc zjs#bPd+LRk`W@PcT&s=XvmzDtQG2xu6bNC3mjtB750mfgD3nKWU)avsaI`N@nCIOs0+c;(UW4rwiyS z$X@h&0sPTVLL>IrQSD|}h%}{xZg`N-W=l%gu^XBSRHuqhTQfs%&p=?7A$z}=OB;x_ z+%)S zD>X+kIKgh_8&}gli=@Urm&$CCl(;DxoSSM9OwIA^u4g0#6@8cutJOrQ^g>}^XD%{Z zoljW^q8O+XUDRInpm^%lT}*B!c{=;AnC9u$`>R5uQD*}0m2Pplr$c2QMog>>)(zN8qZT|A}QSMr@CaI-4p*-qF7Hb|$G8FX1V#Jz#j|?8{98P+q1Agy?4?k*N8BD`nN=iu0Z}P$ z-ARCP1B+*zH!RPWTNR|ggI1172sfWXifOH?Q1*H%k*X4#`llMgC&j!KsHEB@DrzuP zYA4}r^XI$G#e=BH??0@^^UWcL{s9{Si#te*pjZ{@3rFkL!9Uh2kEVrFCUvMqXrE}n zHEuUYrO zc(G`ZA7d~R(WJZ2pp29314uIJQDP9NHl*yp-j_+0my9R0J&*J9vlL|37j!wF!Frm7#65$RQ*e?wvJ&e@UIhby_q0D_?L)gOx6yXBH=1i9wn@r(3lR16 zo%u?l)Z(T`+sM%G6IL6%?z+HScrf#1n^@w{6HEMt(ow?<(9$5UCmq{)VST6u@K?n% zk1nzcLAf)Q)N%k}xYBwX37IF%nDO8TQoI15`H&k0)_-LGra6Z^;~x&M%o`l!RNr0j z!oJw!3DG$^HrysQ@wDw5zX0B`Jckslmbgf{z?wFsDU!7|w7$EvhC9*F?8WF`OnxWJ zpSBivNGErL6H-Td>XOMh?zz zJPq}f8nm-awBvfM7PodW+HeWpaNT+L8d*$Ak&)Gz>v8Kjj{Hwci|)L4;-eS6U$-9? z2<0aO$h_@{etCrt+3JkEEV73tT!G_n2YEyFFLkDs@|2(4Fm82|IUZbZU_m8)@wT$6 z1zOJylsgwaoc&KjOC852Qnin0x$e(*($SKKPfp)`Voo31*PabFx6~p3wD2{lzQh1ni4pbkzx{d;3zJ{GCwWG<@FPAg&+=&%o6JV;jg{r(rF3_ZBIjT?K!b@^9ovG1?*53LjKI%wYY z({E{#BjkZoM#&wjEA~~fdti<7@b5)td)Ii}jhcUYbPC^7QXAEf>a=c2U-`IG>t{{m zV3;QUrUAm8|1pn=6h#w#%4K9kC1qUvt}oSG(@OSfSK#^m!KE$4+ZmysqxV-;r{J3J z8M%^ESSc8|acaAW@xPFtm*Jr8M)aHsl!%{yQu$yLw><&f8$0`(B% zSPGh1Kqdi#OXtIyR2N*WkA%LsuX;f7^62(WLRhc)W_TqDZz>2@vWcaaF@v|01)Vu! zquhjJ-Ks{4mV^&~+pYWXKmzR~p9-iFR(F^|;YWei!q~H`)E$Ahpz;j8bTWOJDr)j) z%#x|1Db(4AsON%o0VsPh*n{jvW|^0EL~O*FPUe^&R50&9zXv;G3+>N8LfmPX@FdAm zSGL0EX17^e;MG^H+nwr_SE85~3PN_*H%9V!?Vi_Ito%{iVR6D8g1^UtaK(ncqZhH8 zE!+X2Qen53A4TjSnDF{YeZu+mfMIbc)fCZfHq8=vD~l|-&Bz3j6krYT?|R>N$Ov$~-E5hYeT_Z^WQNh5DY;*~ zG9>+lGMMjdWJ#!M2{f{5T*gbLr6{dO5&v7^DyE*t!7p5o+-AqwE3zejYy118t-%zY zB+!fZAV}%rsQQJv7Q4N0$FVPeAs|6fR1qD^p#_sH^B6zdj0KPyzgO$h7>92T??LsbT*#~TtJAH2 zn_?!P|EY$|?+Z#%B8hDP=NRsVMc-Qm0W_GMGK*q3<45fNil-NyGMQ9X9p*mg6&$72 z!GtFx;po!7fg&YbV%4r-xpt555S*ID(VN=8I2yZB5YSnS`I&fL7;-6yu^tWqV+jhBWzi=;FKUzKS zV#_$iUu=sBKMM4Jc8-2%oq9$Dd_^Y21imDaboXzeT^{@gWW6G81_DqWIWYY;J`=t? z<~gW&X%WnTH7|MMz^9*c7pKvtd3|o*LD^D~J;t(*tYDDz<>5C)BOQtygI|x;zE{Hg zxtlX1K7ZJ?kw?U+EUF(@I4VZngvJc_{-V&164rUJW+{G$;#$wekmiDZymk|?=DZ$S0Z+CcCho&9d$0w&CDEX8&5eE1-Z4fnuG zj=#6KfARSe)Qa!D(VO}H(uG@EL~iPeb*^t1hc!)a;Ohd~=NuY}V}DSb%fsX!CSC2e zt0!+p?^H-a^dAj8Z?RfTKGF8+a<2-El4RWtnphk5Iy3^9p%v^7BcDDL4gA8}94^b` zTznnz_VeTS7O)*(S8bjNuCK=~WVT$(k}&oUHqgaRO4pLo*451gt-_GL;1aUc^AC+g zZVq%)<@SjDCDCr7I@x}W{aZ0Nm6E8yzquE6hy}>Uf|I|GzO43-4IdKIZ$k?cqg7eQ zXb-X3KKg0Rj)jA%kn=xt0@$k@e2G?>h01r6rI zGksm`3Lt#Lm&n4_LEUPjp4k247fqfQukNE196{CQ&}sbcZ?OgQ#RH)x;u3jj3D_oP z*Ao}nlm}lVd=c*pm_PJG+Lzy^8;87XBT~ZKWk(AUU!lIdX9wjld2>MC8Pw;@9+|%% z?CQ`EF49TM6-vUL47$vnyeVhlzLo0!ZI54E4rQYZ7(0{hh`}MaC&S@^aZkhnw=a>Q z#&u(8$O|R8#>zvlmYtb3U%7D2s_`lZc?5kLy8iD2nnOd0td2w-ZH$y0q9o(q<-`%& znEyMrVQj1~>TJS8wm;|-cTEIN5*M*xma1mQN@{pSk382+7=C;fr1x8;x%-tgyF@!t zfZtYI?-Qk|t*4;Z4EyPdJojnr6_dC3m}MVNBZZ*EZs1tGJ5}lx_sqtIIl+ksqZXB*f;HTy;tX-# z^1v=M>!t3hU!?C!R6i|e7IQ1S{v~@xK_%d4LtX(}GK0Xv@L5YOnPR5$;Qv+S2J#Vq zNYk{|O0HUDz6;|#=-5@(*-_GYHP{SCv~wIQn>-~0=%Cga`{A6gzueK_>!g{5zV>IL z1c9ln3_2M+J~b?EA`%@f1P;8Utzxb)dA;ofG*W5-dJI#S?B>H@4o7S)^T>-1?A{?K z7jH6jyPFgCg%fYGd=`hIL*M21hypW-BK&W1zp?h43hN|bxSP?5|;M}wS*mpVqe#P1I_0CKUWL)dh9H3n=tV+Dw-4a z7cgE17I(s0j$o^M3VwM%`f4UVPOdiB;J-a;acOs`?GU`3TXfq;tNEV8^UinnAtbPT zwJwL(s_2{S(*eQbJ-C>h8ZvyqrzPbDSQ18>roM)A#bJo@800yA5h*z7a*YLyE9!6l zMK(EERyUBOGPmjQHVKuR1?I$N;de%0 z#sPKY_z`}Cz83J?dUZWn*mUdpBJ-WC&GR7TgxW)mP!$G4@SC{K)#cz%rZC)n)1Lz| zZ>j7Ygdt3+>n;a3th^n7iK;zRh|eg~$w}fO$$y)N5n`SI;&TUFCq8%o9o(KEBQU|n zdnS@+?KJ2-vNCL0WMmYe-Dk82IK+C@lWX?sl}Gvlq2g{NuS!zln88hh=L1RieVYSZ z>8LrwxywFkza_ACgXeJWeqhsgLTubA3;BvaWyK3p?yo6b5}922fB`2d9v|Vrpji$>w4D*a z)AGXih0k@)!yF)1xvztQg21pq>!K-ritCGOd1E<*ML!M3+ggny7OmYlbhADtRk}QV z=}swj6SPm!5&Nkh^OXmYUoK=Zb;noJCD4pF4y0%AuGaw+u?toIYIk1c*YWAgy0g?* zn;V5wk39EI$A6RMhlL>K)Vh;W6S1Ky*M7 zHAq5)Zsq~nBJ3H}2n`ee6iV>Cn}h61isH)E_l`-Y8Um;^sfq6~PpUNge0ail0xxJS z);5PQm0myC2Fn&#{<%J0?yZvzurg&;=APLa7nPf8N<_!Go=LbPFXY|4eR)f7L=q3v zPvi@RkCXH93I!P2r0cOb3UF;wy60RV*YOe73`HZ^0bWYMUlXjtpU~v9kR-c$ynq|1 z&su{jdvIKPz-S6DG+V`heOH&f7MtY!3m-#oj+}jf($OS_4QO=jNVM%-u3_++Rodx?JMg{TbAN*gW9MS$850S$?^j6TZ2xicya# z>t6HQR0oN9<}z6X1s*o!ONFo4wN_x?N+3|FU6o%gz5i0f$IdP) zw`~UCk5B?|Yo2#ipBVQ5l$9PsHZNxuGw&hq_piC{SD8pmy%+8W)3>}fPD zuV2YhPwWvtSm179?I=}3<=R^7{g-Uk-?}Y*5vu&@a{u0bsb_z#&eO~W#lNr4J0qee zLR~}zAYjXxU>3%djQ(s7eI)-L{&4CehfKS{CU^7$#%GpZ@WeH_c%_NG#Zm==zHWo+>^3#LCBb&H|mOP&uDH%WKmy&C9x=dSdq{( zX8fXSMw=2`#L#$EnO{BdnV2luE~Dc_hijCK+mHe$tTG zfpK;MF!o#;lcza62<^UFL`ol!?wdG%bRO(Q(lJIJUBa!Iw_tN{?*BB9W@55j=}Y0} z@z1f7>|zs+&Cs`F&@Owc-WOBpNt`sUfnH2uZ;IA#Q#YK5q2owU_@|c`Q|N*#Jczqh z_`+;#7m7-cuOlnRuaS&zoDdsLeN^54_MH4z#M(rP9E=U@Z=IG!qP$UTR;~9nt9g%p zzTBb&eF;IhM)_459se^Lrk zpOOJc1fc3*h5<+O`#4}H)^Wr9?($D%2Y%h@;5+t=7gEXW*L!YM|JF%n-=OCk)#S{u zhiq-$>Tlq8PYp&2nD&Z=_&Z1hq9C9&#gCM`AJ+3`QKN?K`T5^ zDq0Pr{hwI?7Y9^WeRrrki$c^TMi>1C+OPwI(}tdlY`4J7d4EA~#Np9FR{6cz2$2uS zL$h%D3sRH5`HGTQqZ zB~q=nhb{U{`5l7_PCqz~b+(@(Drsy4obp;^;g_e(tJv^IbUWwo>*BvqolYf=%SyxNX?Q3`rLS^|{5E4{j~3VhAdeiFC% zS-g40sq#Q}WqD|*SW5MqaKFOw9bz{6FCfsF@z23${UjqkH;am<1y`s}=`%`3X{qE2E(Fs65Ls3cR^M%FdM zB=pO@Uo5kAw7Xv>Ezsq@iWmG-+etjYr9SD*-u*JgGJGlGJSeMOzmM(cG3V{l1?*NP z*t@R4ijoGWb4n5VuoH{#GpsC)fV%_^Pa8Lu;Ib z0TfmgtyaIsXR@)feTiw($K)nCMaA^P zP<~MP7I`k_YhQGmy3hvS==A#}Yg*t76CdXj)nE&muLG3ozKqL2$#65ULD>1q-g!(_ zD>(Gx#WyEG=Iw+_3*(_A=SvA6(+Xpgw|7l_U%#OYXLGMH6n3l?H2lO^q5A?^pjf=o zGy0&h!`|<1Lhu)LiXsIwO!YI<0}a0cxyb($>FfG zq@V_<^r-T^f80WIZRaM}hcO1!JTtuGKGFL5-#}67&YwemH$MJCFZ|^*8yZz{AdR|s zfx^Qr+N4laQr!RCkT>be>{qdUJ_)WwuLq(^U``cts!dR4jStt3YX?AL9o&bsYBmP|u$cF4$@K!qF zEo_9v4d`br z46N0P$3%T<&kU~{q4^3{Z6B1YnJLzF5E8QN(|HwPd*6P3d6MRF;N2{;LlXQMEGk7z zsaS?|3oRwve-F`|oObI$5_0`y6%0HNGklO?sKCbLS5&(EeyK<7C(Y$&!KK=x?mYSY zwQNeCXy~Q?4_8+i5M`ru=}ze`mG176kXC7sPLYyYU|G5wrCUHmT9AgNLFw-9t_2oY z;9K7BzW3hu{^i%hKJ%P8b7tlY393zfyoI2DD_3~G16X6HbzaV62DPwO@cq?8XW5w0 z&5N(Hljh@FvqT%>ulQ>I2Q!{b&)y{s1*rw%7PosgJ0IgoeByrHPelnlGp!g%^_K2# zben@>1dIZVF$&06q9uMa0LgEIFChwz?fe+kS$Un2Z?ux=1iz*6NegM8N&>h0uDSvK z?1vs3Z)NcMq(f{q=25(1;qW*w) zLEjz(Yqva>;iLWOO*!N+-*A-s6m+-5wuDFhu@d<$MJz-R7y#}Vvz@ntB+6w3#ZC8( z)aaI_<`FZ8D3R_%J>+E!UM3^PC6$QAwXm_7`k^v5%Mxoh)SOlM5(mj->*>a|$b0*Ul>t_m|N}rM1H^`!ba=MY;c^%dRAg3Y2=LL3kg)5xG4=osCRG=tOXC8Id9) z(Da$lZ5M-pSq5CM9N|(cS3vP{RedxrXImY!W=q~i@|b>~$Lo|x%=!RhYC9?n=R@mm z@Yt$CseSMDMabDsK^cz4@1+~StE3han8{2%Q|ARR?fVDIR^Z0VcTMRgHBf=u9nR3n zfG@gJs5gI}b4oswjijYLxZF>DecOfJ>I7Wt0=|&boVlwl;$7QoWL1dHEBh1ww0!)2 z@PDAyCjLamdrqbi zrb6~mg#gyzM42&KG5bSbd$m=qidm8Pm15RJ`2!(^mJbFQK$q2oF9Rl+VGkg5E-kWoWh9K$_rE-?D_Q1TdfVIou zQ(f#oEshNNL=4zorCHFx1fuXavI&IW0!Z1sbt;jP@(%Z=FI@oc~OWKe7%`TiIlZhq1IZz(JDHRoUgYfnH;-sZX|1wS|JBIr86G z`|sraUpr1{Z_O~_Le-~Tyz^GAx8uXa{=y;fZ@Jj{OOYf3fJwDt)CPTuJrRS9(`v5-|yO2Z#*+=n1bfT_O8I znotp4qQXUe@q+5tu0*mXE!lG;A~x;)myEb4TSx96!rJ2{26LN>6Gn-Yml9`o_BcvL z-b@zYKWyx9%}Zr4n5s;OoyJkgb6}>Wlzd9{{?M?^J+{%8m?{RM>BGa>_)W}ro;0MX zIujlF^#uV^d!VabbbJAZZ9OYuI?|82twuWO$q7Tay-7Pv**Jn%3Z zhFVueCen>*a>iTz>egjM{+=f$n_T)CbCq)EWhr7*T2TsqH+L&6#lS)s-leo50bH$N zFL8+vy(ye!39|pz3o1?WlB#BX;h2|-`$lE26~vUn9x*%m+HytJ($tyKjoUHer7uvVrj zMtyrioz}~4!2FpC`jOpA#&s^>(aXc2S?OVG3g+GfdKT*5xWex0Dv(_Fypmqu`dMXNguwu#h#R_mPQ%lA4+hGGt zy`?HJl)WShjwp9`Uw*4ZP$Mp99Bsk9M))cIwK7XNBeb6FHv(^rIHx-r3wi-o29opO z#1ED!Q}5pa@nC3U>T05(2OHX_B7`G#ULpvepE9}*-A{Nx>BwGSU9xOAiXzCcmpPh2 z9o6cjv> zr>OHo_zljc^uGVFL3kZ$_UTD|*vv2pVXcxS);|Zp^AUr%GFKM!2wuuWYL83S`?ek5 zyhkToc5s0cmulk5 zF3Gx#*E1kllX@3}1o+g_GO|t;{3lg-5U1`{4+Qv{%H%_4=a~ix$-3}r?>j`l>9+|<^RtPuv{~0b%wK3?BXrt@lnb4P;J4Fc$J;yxtx3A+k$3yO`J_W~F zd@!5u8(_7b#C7(jL_vPJEd2TY>PI#M=#S}-)jk>+CqD+I+g@0`1a1)GJ5@UA{Jk@L zC3F<5gi!gOI;3PNWGBG|(zlJ`O)SMzitnrPeu%HYVWo-9TK9cyUZia(!AGQeMxCV~ zdM53v(ehEN-K@67Vz_wnK>1cbDoPAO$ghAU)~`?CfrI@ZmkZ*4=2@Fmoh$1rjz!Jm zfcEH%v{?C*hJN40;o*a5lp#&va#`0|TW`~w6J~!iiJd1b*~YvG-tE?O+Wk6y*PWd; zzjouc47p>B{do0;Hj6x5_feuR(7P5RT_tmTH~45RHff2>;)GK88M8?U39O6#xoxai z;oas5nW)Rg=yqa(V!6*7@1u}ORRm&wDo)O>hJ{Iy;F zMhliNK7RfF;pH*d`A_Pn<7n1dKPE-%D=ofN1mCFg4MzV>jqHRR+%IINR&V5EKLcq* zzNoyZ@tOO)ZmVO$lxvMT{(0jJl$l+35FqXha8(3tl}jx1URp}K9$&m<;{uomv&YUQ z;Z@CmMmF&^zQs-wOAq@G!5tv>cDUpX;6{vpm$&U0PZ5ogh-2~362}1xz6IE|8+q2^ zb?8-4CB4bnQrVqTVhqyzqlAs^DAE%Q5iuiONU#ZVCjlRRQ`hoWPxz+lar&TdVsfy$ zgn~-OsJnLukv&A&V|$nzWnL6AHOc11jKajJ=Om5%jh*vH-M!5zShdndX6lEpT6e(PXb+RGRJ>&^sq3j^n}$pa>H z{^jU_Vn}*I^x7o^GO|DzA}D)>HthqSc%8<0KzdtLfXeXL)}i+E@|{}M>Id8>eullT zxM0SzY9Oi)&gZ=07Cm4--bppt(Jj*-qZj(8WD>y2lm3+S`C4%tu|5)@LbaYcdI={N zS)+6!nk5^7-Ve*a_3R3kjD8=}#b{Pqzg|g(X=84 z#3rPFfCCD)BK`B(_lPj}9~AabS76^yy@Q|C;r zTLKuWh|{vwM_N6m3aJ*1YyB0YX?YcHiqf0boxCBzjoNMe`Q~2nrYE)Bh3vf#{>1FR z32zz_(^K)r^olRypEX?>Ye4YXU5uXv@D1h(-EWZo$F2^ZBaAy^h28J@D@*5hIzE%aTstv8>vN z2vpbQ-;0B~rl|fYm2{lTZKqd3f0XY!`OS9n%n1B2s0Y)Hwc}~wbOmoHSJZ0s+=NQW5VR~7KLq9rH4-`B2NoeIdS2emRGMC1 zP+?OZDQn=WdUXBZ1Qg`GP|M9UGiXJ`vstOb_=KVeFJxKF%m4Y~sOg&_$j^mq9}}}YdxgHvD7BrerK7ZJ`v}eg8j=4s1okg{k$+Np7(xe~ z|A{V{Slz*AkZ!=T=ei%`a&$_{v%4i0>dh_?A-o>iXk~)eLy?=9r3Mz~g-CDPBc328 zx@ zX}c3*OY{46Tp^V6mv_1R-uH47VLLKvwS;;8AjGh``av$+Z|L{N`L3{=&@}t!{RfxQ z^*(#I5LwA_pSN0Iw^KeK}4Wlmxz#uM*D_fhSQNbg0#-ke=kNyy0QSKD`Ag>9k&NNI&*Gp*I$NuM8Ay56xyGh>M|NIFq7`A_8 zC&2HG2f`LUun^OmNBcynXfn=y5_Z(@Nj!gIT{)&_IEb6EX ze^faU>Ggkg=qKUEN4sk~n!k?p_T>fBZ0Q%V>xWD=8_^Q52E{q);)U*2SH?q&^G94f zQ02%1?A?C}6f=rZjnqmV-3g{kbzP8OHM>w(60?Kj*mj`kI4naBXczWZ3v#-d?!=I^P9n#yU z8BIW{Qqa9IcqP4LM_J0cy6w3|@BT_aCr=3|@PG6-cmgtcnhG~zm(M_=QVTqS+bjjT zi{yY2HqQO{ppTKFv_O237;ADzoXg;TGBI*ypokcq<=B}{M5xfg@1KfP^twoAXioNox5NeP(r7ZmHiLl&xeudJAN`gO%Ji~QvKO>g)GnHo{|1BD4Kf#EG{h;|jV5AC>xV-}!K1Q^0f2@0uH#doW z)@<-$CPpY@UDjur9DbgrRyo*FD8_d_D%W#zyhEHI-;rPn1q9AMa-TZJypdGjDcvSZ zm_}~OJF+UF$q>%<8~Dt`SBI37tR!=EBGcltXf+ti`CuOMgAN6w zQv|*TPmG9*Dm!#U--MKac<`bwP?;zkM55s-xR6MQrAuJr6F^>$rMeU(L%e$(S8y<< zKv?^e-HWYo{}&^>R-Lh{FIz_@<^qh-c~dHXM9R#5{<69=zdw z|Na?d9^3C{@NsM4Mi$XbrlyxmP=>#;!u@v<|E9`ww-m^0=@C}lnHkL~$o8ehivq`x zU0^_oP_y6L<<_$cqi4nVx|70d?h?kD4REwTl9&iQzYuv`978kBljLn9p6LCX2>ySc zW8%$I9jPQ4Er@FKCJ;GyL^Ia(5d1t$;%rsiUwSiIC<=;E@=@eAKTTAb9#OAP^t$lH zi0j>vi&=ZTgKkCRLN?sMZ6GZ86~QBy5!_4rQmL=O+ihDZ=@}S=K+zcmR#wl+Q6@?3ALbX} zy}cq$0)$V$=1Gkl*`e5ex2@gN=-(l)=ZwsMi@(<7FWQ!I8#-0F3t? zatii(4Za@SqV6#bJP)}U;YQjyeqtbKL#g{3VCf%BXdDWX@K?7abBfOcX~Oo&Xxmo5 z7EGE%Y?a1)1`H5>xqH&>p~?d`o*Z!o@)R5ufAD$Nnx^X*sDyF5sBKo3>Y_zd&Yc&Z zXJ8><@&3N6<9DVd9z|*Wd*L@o z)9CAd2w*9XT&5){|I@x4=gor*_|a;x-^Oe|c*DU}-HDQx?>k)*5X(qSo_%e&O5 zj#0Ydp+B2$C#UJ;0_8E&dZt+uv?Hl=NFEGlEio!#jbN4pcyv&hTiV<7O?6zzj8MIY z-(O(#N6>EAC7V_DpBGVwiGi|A-%hBq$j(oU)D-vnzJPZV|0US(K8+cBIat#ELr4sq z(?ft~qg?pSkh$5*^s<+6a<#R!h%YqLaPN$mG(0a~mO&$hX?6$r>b*Y%m95^c&V|5^ z%EkGQwk6)zDw19LI(E3Vu+{a0xEPkNYWiP;iGt2@AF7X)3|SV&GHPa=FlI zZl&Ei3@4;;M08G0T~pFd*tkg4NcHXft|Q}}KtxnbFoe!O|MD?}(DKjk3pL$D#T^Nn z0vp*7>#w6`57q~S@|8`DJcVh0RKozi2uhiq+LaarrVBqh!ynPli%xVp}L6yx|1VJDWGJ7jaEcS1>g;WwB&9>pttoBEXM-!-F!?}25r z=(svQV;u)ck9o{&ivhCJD9W(%+#1Z8IaL%^=_!W##GqPQ3IsZY>~2S;?27N=M$&I( zR2K5eZd9N=UuCJ(4MgEyGEy%<@B<)UhW>GvJ^%M zV^i3zYIeohIJ;93bI;1NA3uAE0cI@!5_;1gFhW;m&SK^6DQNix>-GUV!|{G9w3{0Y z(x-@eJr2_T2(>pYf;X19`N-0EhoO9l-yq9CVx*H1advWPAZ>`@IPdhSps-* z4csBx@LuV2M!)g1m>2HsX!nu-cpin^I&Z^{m%foCkxm?~d%!v-o zfR8?`^e4YMUucmitHJKDsd3P`=d)Z6Sv0A08@RlcuNSjdA_!T}eJ*a5LUwL=-c0rS zlxXepy*;zJ^=ZmTmHQy0QoR4nm`R?df-^P%@!i##(#GA&+Z-pO=e|Op+lkl^9;o!Peee2pyBHI!zMG?rmF%~$d_A&iZ z;1#W8@~{bk_Kh26h#QTXBH0DpI}bkDvVr^GY$zH2Z?+|MuC-NC1~=rZ833XAqgj?Hej}=~}4XJc%{2pCZDpe|_tg<_Tx>%auYl z=Je{x#gy>yOQIb`ojZtmfz+=nPR<$WDj!v+pg)UtQ(U5{OS?6F17w}&lY7j$?Lie= z{fQKIDGMk?wO+1ENJD49z7E-%M4>P@!Hjornm0lyREzp9Pl|^KskC+ewH4Pq_-#Uvc}Xr8S83C^%3`T1qP9P=8AM-d=PS2U55$ZCVHQn zGe59jf04&ei_$uu=Ie)>3m+wF_mk8bm9h0onRjrhS2-7QkrnaP606Vva4VXFw<|1? zh|d)hEmXeJow+>s4jimN>64pKbZ~)cKl*iB(dbN3ZD2xPzujo7=Q?w96p^>Y9j^E` z(hB`&()BNjknVUBbSB+>oKXslb@O3P3Ys+3(dG<=LFw}be|vz zf4U-U^!a<1*qgg*wsdZ!y6|s%_9o^b>vCQ#QpJbrUXfA+h(=1#{%ba zqQy+}HSp3JWEtf7d>K}VpXfz&eb)>+lJeQZT$i@HW^M!nb_t)|l^Q+9-yM2slK47w zp->T6H$$R~c3+>>V}-Y-A>nJIns`Ahz_M!26CZ*jU)_KY-=nV@D~!=8BO*PEA~4*2 zar)*9qg+)!3#qbUV zAg+)QxrCj06~BS{4Z@5Y_l`xZe!@=R!JP-sD^_~SGuA6)wt&|Rha9;#fH%Rs9czQX z5E!K$&DA6V+mRmW?2bGEM%h@u<3)=>v}`BmL5m60Btm{mZrW%G6fiL4!a8Wz&d)L; z?7}dih-O^ySo-nB2;YqbH`LKy=IFG<_eTk;U7BJXoo3ojksIifL6i0;($E#Nl)cSG z!!>M~P)NfpMkN)_sz7o+Hb|naoptYRl*A?q$w4VcNQFLQK%6Jwtl+92q5D{2Maf(q z@cP>dsx&QGpM9!2F$1jXRRZaD6l*Vkxkq$ZV$J6mx*Z~pc3Z{%H7st5>lhWLJOeL6 z7FE6ck$@ruL^BFP+;``1ni_Q1BclFbv%LmeKmY_wnrN>qe}CWZDpjpP`(YOjUE4`9 ze;`4~7Yo7GB!8LKg^wt95DEx72~&p>n7pbZXY2L?)?%=~eJs&za`(7U{@$;-=w97Pce81Vf5YSn)ESt;k&cW*O8(Gz#S4Y8>R zW=ei>3$PXMb+9RO{Qp<*uQxHuj;u?*!X8ZOj05R!KV@KQWXT~#+-*z3Z|dNTYeKqF zm`NLGoL~wc4K`9XOKv9yAPsDt*%8$U6jtTHY<8uI4uAIYMkoqbA&j&>JkN5Q;hpUS zA=d7x_;w=IaQ0q3wqD8rwvNcmDh#%R9rYG+Jv7w;#30|9*E|bqxuG=EA|chwkE`!R z&bC|4$HPx*G#A2Wa{bV0TRs7eye|0Bq<-OWh?q*xl6%GJjhKBLko&CGT;JxqydlR> z-cPd2@jf+oKmhbByUTEX0QI+pNRGM~?eKU!H#C%kvEQVjirTIEi1LK|r_%pN6{01* z_3CGBa_~f4bH}*qBYq6kwX?{cRTFbMUtdM@A*w1z^&~M}{n6o%b8*1$gVtc~#4wA9pM z4~9&m>m;3=V;hyR+J~bTT+}z6;)W~VLJQkvQESP^H*Tx6-(i1t)4Or;QOLk!Cesp^ zD}Pf^T|hf}LG)bfJXv-c@PQIku91xyGwh2}MQD~lEB2u#dLn}q+F{0EA|$4aAlYF0 z{F(VyQ{$l=*e1Rm>@reB0=?0&g^D__^~molawTR2#GUcm8snb~g}yC*=ij)+hndj$ z`Qk@qeuEEpvSMd=NaUtIU8E#$#30;D!Xt;o(JzP( zX6J?~7c4et3I;3ZFjBC%<1@1`O`&X_z9?fiARr)(vo!o-OGn)Z4BqRp6gVdmvxAZU z$ZYG~z@18nIKQVRyga4B(F(iN-!(xxn@}(X$IZ~5MT;tQoz~7hN5&#tj0cp6b|g0h zk@ZV-tT?<8SsQ5fz8o(6V^rmZVbFZ<4!7exvq?fK`+VH+@ z&c~OFZdIaAP(ESIzXF^`kk>5q?A^p%mGQUNXjg_099~SyHN{l29?JP*B~HF;l+e5A zJ-qX1kiC<>du*eq3QSUM%uE}&8U#4!2y=aKLhL|{*kjVS-QzUhV<@f4pbc7DkA@18 z-g5ylAIHx2#ko5iHJ33;xnZ2-5))Zv&EC|K zjKJskAxIy8UE&v$$J5O;I5O)euaKnP)1u*!_w%Hhx)Nsj4O2KWu-~_}UWozlY{KDR zDIMIfYgq+c^82(%unp_(@ZL>hZ?x+=yxP(^Gh@8Q_}^%5pM%I6b1mLG(TP{?KoV}m zy=|TM74)avazHG(j5N}}_-z=wwRQvP^MKtV%ch4E3JH(+Q{;9WnX)JZ4<9E<0va}~ zA&c)qev(-iGnDmGJADp@@vj0{#CC1Z_-Q@79i&3g5@}c1*Fz2x{M@&GI!rSuV@Q5N z`EE`%NP@tJk4h{k$tAR3d?HDhk8Hg4o{nUVUz(h{q|r407{fGxfZc1?e6E6oF9CkF zPvIkuraYE9A?r-^CJkbg!Gta*1vw*q2fmW6BhUMe{?HmG$`4%M|L}A;ej${56>xkz zASfRABTx=jbt=}vQG;O)AWv9h4$tjzEO*BQ7(XxEb{Cn%gPt`2td-OHby*c*Uv{F; z-E$xNxx?x^9bB+3l1Hkwd*4V+kpV@tD5f{=yfBP_v~j7pc&o!kzg2H;OV8uiH=NRK zb=-&L^EU@SK(po)#0SX2K{V6)yP4m=wu#3zTh|H@rLX1(dCQDAdB!|8OoOB|moMngP5Z^eJvnFc-z4?TD2A|o@2zQljLrfoQHn`qTs=!I5Sw)0z>IvqHH zdfNp$+D~=RI!}{JRwHu~f4_a3ySo>xc+IxTQWPpCmQ|;3_9`1%g>$O_T$5phFoj&6a9 z?<``ZH0KedFL7e#J(;1)UP`M*Gwv=k?=9C><*x_e>l^CzK_?(vlu_Jzaj8!3gchuw zoNKCvy?9|O%Wt*gSR0uXm248uaanw{Iya8hH`7(i{kUtf!Y1L_pr5!c54R4I%6Zv&}oUEKJ=_had1X5BC60M-z+(X^rvx; zft8jljQ|nQWD84Co5CvKT>u}q`-}})x!{=y1q@3EFZ4Y!Xz?~{rxZ!G)U6?@=)&R3 z#e1~?g^i*xoQtxY3AmFb@j-%?l;mw%->@vwZvGvn;g3;hNwUDayn5E^eH+G4dA`FO}MYOFn)QbI>kwxuT1Xtp{ zCp>Z&q+l6zCF495I)%EDN{(8Q^cy2hfA(0#?fCXp*WX@6^BJ?sy`?BBjEGT z)$r*I;F`!wwf$whR4)f9t16eKWLQYU+XOE9rGW(WZk?9e=A47RX>YH4q?csct~oI4 zSb47)(?q9Zfhq^r)`sgBv{h3AR%z4~nHs~4Z%VQ}!sg6^t?NVQ&h$1mg0VIR4ur$m zQNj|J4#^)~%kP248(}d9BTp>yUt|)|GA|C1J*LvK?_+Pfm5o{D)-ao59+iy5t5urX z#g}5OP=XKU`d;bf=Ch0Jq#X#h ziy)R3$g~@m#DjMGCGg??wyP-B82kPXKj7}< z5gm58PiaUSJyk}jCS2r{V@?YwN?hGZq z>$OzbwuNWd?W}p0zte7eZFc~1?;XuX!SCV&W9yDQ)7>Q0GL|~^)Rc6)xA7Li}IYbrafty%oztMlUf;lNoiujuD8)wX|Pu zO`D|2s#B<rAYm9S`^ux4QpTf`}9e6u({L0et8QQ(Obj`bRc!X@`d#37P2 zCD$tgW9B)*-{`+S`7!I4Fb)a_=Z0+xl6a$p9^drG)$Z}!-ecvz4Nt;fQ?RnJ8d9Q6 z!?e>X8nGQDt0Y_RF%^EewMQ68yL8VsaIpHyI94zm<0NYsdN+uS-i2ohH5fw%c#Vtm z5I*AOxZ=_Nbm{|mvM|2DPOP(Rsj?jZXRLFRS9L<$%*J?P0=dZZme%#Cf|}LWmuQzx z&t!$0Zdq=DkI_DcX*&g@?_4El8fi95WI$_E(a!dFJTt3(a!O}!2{fztanmR~X+gx? zlL@h&(b78=mZQIyC=T=^nZTfl>+l;iZk$K&qu0>YuGc_h@IY#OP&)c>)O7snVRc_s zEO3Fg?j6wR+n}?d4gI31Nm@d3naQ2g?|8CJEEc(Mp?oP7d!&b7vPu8!o=TOmkl8R7 zXZLlZOT*Rr?R8<-8R)erv+q+G*k);#VjzXAYHohFbiFtyxEqygN`WV}%K}{u#y8u% zT~TqlwTMBPB<__7$0-9Zqg73%6!1hzeZM7msh)v?jD{0pYz`x9tD-yvujET~4*A~@ zWx1@sfz=Bg)%yK;4Gv?F-|m23o420$H+ON|F5iDPA(y^~Omtzfmtl-ZUUNu1!raO+ zZwGB{LVL(epDp`8N&+_5!p+9fr@$QVbO0LuRJa-T1S`3%_g4!NR{@B~W}#bpAxgA< z7|Xzm*yp(vwpo}drVnnXo1Zn?vAHF3XPe&$Y$>HVx8Cc%fV2l9o6kKO zV~Ah8q&IJ~YPh$#0R$_}C)G^NE%7twz9+54OX44P09hW>9aVqWOhZyleFz`UMtq-1 zL>ahrgqkgT?}WQPK9IFQ&Z8saS;XdA9gZ7!0u{&%h;trKE&y?~WD$z5sk!QApYf1i z&^{jW^MGLdvApXtZh72G5|@+kiKFN?4NtyD^5UVJILk8UzlnpI23jG{@)5aa>2PfZ z)6a`U4}pT!72M=d8@`x+(ykX!cmIILv3-MdFI=->^~E>iJ{10VGS3L_S_rs@`N(-P z1D=luWiQT^X1bvL?9869l2QQ~yDK2%#^ZnrKg2k7X@-27&4Ib2Td=3ElmWq=T1DSxIA-RmRe}`Mxx00;w1{hAx8>i@2PD%Olkd~iRi9F<5DA# zZvC$R0)s%>Cn%upuYvP_GBhE>j{-O}0)w)natUI94&=KsPCBi`+c#;%{n6T5@P{(4 zN?=Rya)IoU>h{lcSy-YhH*g8g>_~P~c&$2}IXB4>>0y}mRujqbQZDyBi(4G7s5!K$ zf&oiw1|&Bgv4kan3=745R1kyHovCGjm)z>f$pR}K?D{afa#rJPFb7V|hHyUeStyc= zIfKtJSv0%b5=10AMrEVrAUH*#k~6mXhvO&M;&H(nH`U%Uzk4`2$I*GLuB8;Z?B7d- zO8%?fiNq3*36!!9z&hOZR>A&72ZEXhsn_LWb6`}qU9mk;u*mFnJ!4mr*^^@QjTIES zPBg0WE-A`NdmOAhi^-cEx~R}lO?te067z80(Rv(eH^}cQNY!JYZS}6lBeK#(s_5?g zFi9_+>#S?l;-}oM_4ysMg*Ta~kvOyxdPaO$2xrmdvH3O^$-mPXj_)*m@S{UU80Y#Vn zycn}w#>Y;t!H??N?|*-T?Vi+mD3&FgRbI4SnmrNN^r{9^I`>#QpV^1gL;qgi%j8tk z5is65$q!k&=_7t0y`sIc67NOa=p%gbO>%MxfDoGZ%~q<}0G;z8;@Z~B%i!`lZ1 zP)O^MkT?pMQPxMS9t?gk6uBrfeIqQoD<*O{YH5~_N)`yZKRvl#&vStODCG`z8@`YI zjx;v}L(Thq!O8nF_HPA{wF6Eg2I9^D*n88_t?mb-KWVoft~Qc)E5-}J)Gy{=clSM7 z(L_$Ciy#`syvZx=e~;1@oB^Alu?I^ZIJ66A4biWDoQ7B0{Q``C!nk)NxSwUd%3~N}*?EC+$roh>;`yp2 zJgS2~q#K*tG-EU%@jO??=2S4J>Ny{cWO0cM3jh-;W43Rrp@i2kx`*(4u=OETQpun6 zIobWTPhcE4F$L0F`wbLM!)e=M4bK6^sf@b58S$K6ME?gU;fgA02jEhjY)d ztk@1*yWz0X{5g0&%pQICB_p0V?`2iz7ZdP4%v`zVZ0BZj;)7tM*<^$`Ih*h>9AX%N{*=V9=J8& z(D|dmHS*zdc;@s1)9~bnVf2yB&`i#{;v2PT(VnphF0p@H{EMW)cxR}>|&q)W{v-^*<|y>^_pxq8^T_h z5u8z{v(gRe`q5*jQVMM{w-Qe+7R@+|f;)XFNyxkaa-8SYHc@_>@%HRIaSE_XjplkJM`ZD`j|BX4U;Eo9`aDb#v%TnO6fa134pahOd?g6JN$vQ_^MWc z_hyzHah0ZHUw29k%~ePje;mOOpJ#Y?G|J?U4Yc_VvD{h39yR0{iikMdax?Ps3kTHm zBHZ%eNv&fi#4{HShr2EXafXx7hpsj(#d(LSdBUq!5j3=sNqo{YKbhPuNl@qA>AW}{ z1arA@#~7(*LyHnb-;=~yN>cHx3B9Q+S_d1j0&$djWE>}PfdxWE`rEqgwns=LT{iO{YV{Y(M- zd(1cUjO}n+(9d8P4rsf?4Cq7Gy#0gfjW7+h_oVFU^@0=T z;k%}b*Bmj$*|YXDFXpSIy<%D~gdXzP>-=c_v)2=X<6ixOc@X#_z-}S$*5(f?aB$#t z!(E&dLRjU4^}{dhKd+-2J$U?n!tzRN=y_@zsr=1DX{AQMQ2pZQ3bKjo+$9Y4`AHsSD`_uwJmglr*>3C@@~J!Y%u+*? z#!Jui)nH^{Oa1JJf;^G7V|f)M=$Y<9hYypma@8wYh3n=+F*a|m32tRLaSU5eNxp4W zqqg&c62(iGpWnU!zXQ2alise-Gnjbt`(qtLfOm-H|5j7f5Ygboa*^j_cgy=&MSt_P zbArdBo@bBCT-d15p-hIOT+;;r0!%wzi=cR>SE}nrt#L=D6?PsiU&caUkI%4pBz>D_?Ok*(0n`YP+uSVOXnSW9tAk6+Jk-r3ZWB^OZ`;UgTgB?gS&ft7!jy0 zxAa%PSlVB;(#p@3o4>sP{-Y5j!ZiZt!hULa)nOYDwXT=zi-I&!Ui_?pL;c}fV|pO#;}$6pCP3a3N4FV@7qGx0tEpAHz|cY7ye zqvM+h(ta0N#)G#l>_*y8lhNV27Vp#)guBD$Oys@wYEf_%4`^GKze|D4WQeKtVtB!2tY@c-?h%4c9$_j#F8G02|0vp_ z>S#Oj5u+~`KwMvKV9}c`6Mgu)dWcB1Yr6dH`=a+ zNgJbIuRLn@CUd944vW!m7I$aJEBd&##>iCd(0SI~uIBb;kP#k$7wRsZkxJOqC;M$9 z%hEd!`Qaz{KGzk$+Ny;P&p)@$DjF}IhOczcRnuojoy-{6NF>8j}@ zIcf%fLa`Bpl%vy@{GRpT+(>SM{m7$vyUiE)BSKb8$0{WnULHGL~ z9Ml}T4eJSUx5rDy%TupY4kip>0`vzjU)y;VPf(iV6U}(O?ZkE-()_u)Lww|dieF?! zqPoL~Nt{F^qgpt81$P{S;%ss_(hAa@HSKP$GY_&eetjV=T2G+oLkL2ajud7KNrgoY zi}uf!sGW`}RAf$O3cPvcw2l<;HuD1THP(e4@Z0;hON{nj9!`!7Me~iV8zJjpyWX`4*hkk(;JbRcS=`DwNx3j`>OI{h_p@V z#)@0HM{_^t?oI~PJ&EIXf2T%VTEyZU_yIp-+bG8~{Hr8i!#u~tv)4E4(o;GJ^XG&g z8x}ojPRO|%7@bA05f^zM_A0z$iZ|_Ick0QqVsmj}B)c_H?_H0Srq^dB5L>gjP%fJm>^2ki#vYTJOV7BZ=&dsbX zr=1s@HsODF#QtiG$&lgbn*+%T4I+5qEnr`6e^U8TKc`uYJ68s=6>>7J=KyrXl&>5J z*l%>`!KeG$R>Cx-{mn>7e=N1Cjar&}dY0BreUXOYia(l7!#RV7JeqA|$Bt*fi*3W4 zg?fnxU+975OlSXAU6}Q@LAnRgG6<2@cTZLy^e|;^2By&W>R4j#_*^OR=jhDRG|;xU zZP4*Av1gck7R4);E~>dNRaNI1H@0@`rF~#EG&c|qR)-S*(B;2Y zX5{M&9wI3}sk-Mn&33LFXl8M4Ua~n|hx@MhGjR|vL6nTvRtWy87&ow4^}63@;lstI zE4s#q`lOrfUVZN(owaskJX=#XMag3*Q)7Tjj`wzG?&Bc=*M~w^yF??hzL%@tc;NbK zo})P|Pc|6pQ;2V>YvdtP*0jGbv5pLt?bj?0qVbn12|a7fZrD;qe|0Llld}dkmiH4i zog6-zl(y);HqWFxk+Eqws1RR;w*1!n~A<*LQ~MzAslVqU#7`Twr5wT&6}{E%-Y(oV58OZ!(?kWbQ?y)H+Nx?%cnu zdJkL^l$n9>L#;z-yq3kPJQl`6&uU9&Dw>^hWjKFrj=L;YgUuvKhMnuXQh;UI|$qf2;X{zn}DFd@5ek+!z^uzp-a^skJW8Lu~?O zMLl_b_FeLp^k#m-J*qfg6Bx@K`ks2pCHR@zu%N=CVZBnDl0fnY)czHI&ASQKvjP4w zq}cNgf2{AKk~E`E7)M0~*7}Yosvsoel`#r{wKdafL4(F7WGh&#TZv$eAti>}g-<_$ zfUQS^&&{r8)pk*9YP`k;dz=OE`ei5bmP;xB-eBfXWX!i@PtmU1@Y|SYr7Fy8 z^4~a<&@~9OjKn654*0!J+49rq+|7X0adwBt82p8r=+E!;`9Qn0CZ2OBod~hH9?s?N z4NY1S@|q5{lhy6EQj0r$ol5(q0&DL4uuYeN5kKO$dg^`7`ZC<&`^?OBJs5lLVjS*d z2o$Qfp*T4YyBkv3A9`x((fESAM(}$%7sn9?ic4|W`D8q4Vsk${*Rn#RfBRa@rw_W} zL~)cef)mX2Cv?dL$+?4OgYL+@4=^tgJ{`NOhq0BnGZPAKefFIt>QH+2)Yl@&WZHOt zI?JjLAjaE#T<2G%D(Sc?Q+*7@5L&i+-g0?mB*lE2yf`Fu&wH)euFn1tlc_iRZ`%A< z|7(=yY3_u^nbaD`eMqL!NJ4HjHcu=_4j@Ew)fs<6C#nW{Mt!v;ysipeaJ_~um^5l+ zwHn*~8tGg`MeA6-u?;%m21{}X?R;AHygdqUA8?i&ssEl-tXZ$zO|ngftp+evMg2dz z-a0DEwSE7lkp}5b1wmj4r9+UAMv;aA0TGd|0ftWLMkI!mP*S9G=#Ul=siBdYA!O*^ z&EC)6-{)EHdjDcA)?(JZnCrf-^E{5vd0Z$xI_*9_r&TRvJySmshLqnKh)$Z58WDig zcj;m<|K!@w&}EAOxmUC>;_D^jCo+%T>M&A< z4%-{{_vW;QuB@!&@l)nzurJ9<4IZ( z|F#?1)CTayZ!wGbg;SDUvBhIw@srx*WuUGhK4w+96{uJb;QYXK4JVqjS)}_{eP+%b zLH7;}Og&`|AhLf(el3r6{h>XTzy8xQ)L3<1lx`kaUU!{W{|MN1)(D5Sr4gcrKs!ip zOEzzIwX}M@E>z0?=UMV%#7W!DUdQya;=igUpwSFvt#Ig3oDXd-c+u(g%gU9u_u%xX zb-C^0Cb95q&xXlrW#~Kjt)kTZep?V@dtr%8ZUv*^Dz5!&z>kO&B2u$E0ZJXz*D&`( zp-ZJehy`y$`)<-w^|@fcmE7gsX^J4CE5EX(fbND{V+(E7=X@%8`7#8Yy+2pju$#0K zWrmOoj13SMmZI&X_&IdxE3qWp_Us?_$HG+@@LbhnJHWplOs8)BiJMN$I*yfn7BZc* zwO^fL6OBK%FeXGgypbGU{PCSdYRw(NGjelK;E^Ie`@@e$)Ye+yY#VEWyHB_rXe{j<5LU(Sa&Om0Qar7BZ$kqE3L8gb0XE2#V^F8pN0}DB$F*Xc(NX6^ ziYjjkp(ehYXOXM!PX(S#5$J?TC=*{$_`f-pIS3=!P<*%;808uIjBAL?)B;Oz;XTJO zE}t|{>34>rCT0Y_!PAX%c{chup&hsh@&&DV_Sd?ncW^QH+xUF^)mJzkY1l}0pN7r! zCwD{+D}*B0^Oyoje-msXvrT9!6>XB7#7*zR>aZK$-$Ofe^$4!0?57)XbTNPxd=lAn zvLq5Cnbs+fMX-za-OU7*oOsyYJ6XWy75j*-c`{Um^cnv25N}ESp(Ip+aMn zw-rlW^Qt0MlfzobKE^irw$E%4<3Se8Es|(?6|x<@iaSzj*Lf6K_Yg5L1HFc#kNn^j*83x8Pj|^*P00C zWUfuy;<~zDVGlZYeh5{}gs<)^&`}2DK)hlD)HbKij2zw)#Tu+C>bIa{id|j5%r? zH{GQz69cw_FCr_91fQTm5!WmxwxJixno`XFNaWz54>+7NV3SPAw_=;LaqS&giiq^$ zob|pB$lrXi0}BtW2{_W>O;r+Sz_R zR`pv^tu^`F$oA%A!1P>5X>Z-H-N1lNE%1f^;ZKRpU}}A|^tEI+ylQzY%re6!4R= zhzsCo0a9Z;*U#nva~1rCJ@X!edW683M+07xM;&ot!~19FiC?q?U5`IoVi^j$jc1jM zS#%vyqVxcHmn=la@<}1ls5j4YmJtRQb0XcR1rx;QJXg)wzOD@_Gh}kd8lG+TpJ8#k zuSgyCt}ULbb&ureSLQhLQWgbK>1^^?*~f@H$;ru_AAy~dQY$?ncnFF4j?M_(0ud*| zj>yn-j%tjb9$mYCKDvC8-ev=|jrz~S~8 zxI<(7a@D&Ql2sv94{3NGG2N%3igRmk$tq*}eS*MfayCWZSy4RYYG(s$wLhdv4|Yk! z1WK}m_?08ho68lP!J*m^p+UQ}vvhf**GxY2ADV{F2qy*+_1d*Q4e-0tu;3sQlBAX5 zkFNRO6P`%I@zq+bE>ZPyad*oRCM4c``*b&|WB}>aG!EbbrQ~KWoS3R8uL<)n)xd)G z0x#{gMDwY7m(EYy;kqiUch^KLIxuoIrMDtyVpUZbZ^jsym<_;g5?Lt5vZbp3_Obwo zgAwlwsEu*@VK3cQQjlvsqzSWI5od9E$f0DC7Vlv7yq0u1q~QU^6uWxRYF9Pl*X+aI zMhE!8@*jiweJ?7nF-(;XH9o(fkI>8Wx)A5SQ`%V=m7}RGr?e4r%=PHypWwW&f+JdD zvaUj70?1cO>wGGm4@HPKg3k&Z=Q2*zhrcnsL!V{O-UMy-!tHEXMx}ab3qIVs)GTy# zZG-ZLenprqvf86YK0$e#E{>gLFQkf;Dh=GpS%wr|{2}rqYIbu`|AwNFLb&u zf&K_v>RtP1Py2V35n;cRt1PnPw1`G=p>O$PiZA_Je()GNt%WC*r60RZcrD1#XvwYC zE^`<6AXa%}ieb$;Hff`(46%ofSzS*eE@Q6BWDBK_mkSVXFq2XWb*4qgss|eJX-A#* z&1nU**K8_m6o6Nd=M5AA9TI)JZWyipV+y7aD9+8<=vU{S)btKk$4ftp-!|7cQ+sEm zNmX(GoDiT4I;2!@e1kX$zg9W3i! z?FB}p2%Se6lUz>Ko7W!VFY8RN*FzI&pULZQOaxAtktDgfZDc8-DnYKAtg*4krYBC} zy5Rz76dJ3zaESYGD*?tM*#Kx3^kkdq0V(dP_a_g6VQL>PktMMl?h39SQ@@C8Mnob* zP!o6dzU!Ckc&C82*l6_$4Vv=!NkX}kK~goUo+|}Mi;y==Nfi)m zpUk+`Ez3|YmXIG4%S)9OYASC&j1?g#{OK&nYM?hS|taAOzsjR zrtwgRuIYD{F!5XF=ShMp7+*j)zmW?zk5?CuD&0&Y1M@|exJ*6Q*z8Cd><7(!~feJ${WQR(sSD#2=8$ zP6H^%+@PZIw{q{i@38=~pOeh{p&itB&!fR2(R0HbxWb0w8z7#J(rtYLnd7C;5>T3l z1D1#C6l-)A4dLnx*a`b`*}ge=Dc;!@c`n%JheqI64a83$Nks-Q5UswCu}L>$dAxMo zSJi!2{fX(UCiu1Fa;{%8_yXNPxGLDV#n6s@adFVTgf=}p`YzTf*qhbi|C04Ni^tk~ zq}i%}su^0BIlVMRcIp+|?BjB@}LOzXfZxSLSy>j+8(QH^w=J zyU5zLJ@^XmMh!EhEFuEw4?S*c_SO_UqD^odsqg+M`VWeCpBvk+-U?dn6MGunHulBP zi#IL8+PgW0|Gu(UO^<6D*oK$rKtY{G0QU}tT`RA~R{=JFh<{#CZ3W6TNk1aElcb@3 zuJ%^ig>rlTqYPD+2`@QD$`hZhlJO=u86FX8s%kw-DM3C(Cp2~EHxyUyc*uKAMxx@D ziC<9@j1a8T+i0<`d9Rg#3~9hgk&x^q8-d)X#C+I0(ehM#O5<_iT?~G8gYwjIr8M)h zb?hnxte--Vu%nqXv_y$_N<)M;qv^*XJNElEB$MN|j+17ewy0ztv0M;+h+WG_97yq; z>4`bahK7S)%$-aSpT6OKE(I7gHy9i^U?9HOp^|0G=&u*Ep;+4N_*PmrzF+&yKdwr$ zT|jvpoh^&)EiNN#&b@tNa=1Y4FUl>G#mM^TSo$u9cu)=B+cXLqk4HHQy8CmS*yNuH zPXqCBNIhfPZE88ox(r&(yNMu|D*e|SF&dcM;lMsL?#Nl3&W?1yTad0a3 z=9is0Y>>JnjiHy*I7j-;LRJ$SKJPSF58OW97&bQ1E8WXpL#BhAsyp4<^KHq;^PfV@ ziQZW1lDUWrWy4-YO9_9=4FSZ?Sf9v-tBeNr6>>_@rhoU?C|DNN;6 zWp9zuUq=EFMz+kvo7n;P_Z$3zh}YL0oAe@ts}pQQZ7F zRP>u%c^JxP@wUfpyFPL1te?5g5tNVjQsush{|vfbqr9Olq4Qhm_Ob|xogOb0tNScl zIR9?4JkVb`GwyrcEI{;syIKkdZYh@bueQLA^ZQ-Oy*vfZ2?YxTBwQRXw{>_>p1Osh z7AY99@kv8pwpU4h`8wir3$~y>2^Hrod&M2tT(jsrn#*ybex>=(o^4Sh#3psCrU_Qc z?O=7fvwuDkoqtPN&PZUa*Pu+K-LkBCH(RZ*7Dg}oe@K4FF{piDAYQ#l0P{F<_zC#? zz0I_qN-Q12ri??~9NBomO%aJah6hU{>-_0qeKQSD6t)tvbrc_c;KP6uQ)M~%n7*6R zUh;kQWjX5Dxotc{EKG@FSi9_XJ)wukV|pki$BT`zpH)VD)@+HXr1zYyAlo0E22MD< zRRUj)jHlsKIpA=i!x|T(v?TBmVKyUqvKqpl(lp+%hM-4uun;WfvCsk6+a%b0fGt8_%sN5S&$m$n@fENi55fUB^C~Ff2K4- z*Q+f{YXS!3$#=;;G^KDx{?!%1GO$-sn;&sM){bJ&t}j| z9*%d!HuC-n)2EOJtya+g6uX*yG3m(;|4nEH#is*C;|=7je!Q3+uLR_O!fr?xFP7=r zPEXZDN$YNFc*l!z#H?~teiOlxEu|!`vQ0$#`|MhHLdrJKANx~CB~xptJ0c@Mj!0_% zTiAiR74RyC;#SGadcA8KG_UG-7hS#m`(iW{AJ#6iiT9cRQWHIQmvda1zHv2#jJs2} z{p`WK|3YiN@NEO@2Mm7wh27(`<%8|uk6YP}r_*s&jF$@(bAH$?Atv!Xv*+#wr&|l< zoD+xG`CcmMW~&JKCuYR+xiY(8_X&a-hT=BYIwKpYc~fP@i~Bgq$mG`77Xp| z`whDzvnp2H`D4bk^YMe2;+anXt+aksl*qUm*%cSEo_4`>%S2WEn3kh5NE0Qc9vKy# zWNX;|w1Y?y>cjWk0iLo`-puk6pqR+6&QBubuD9{T&r(*C5O0dN@1>hEx;I}MTw;zslWRC-_3FgL-_I=iK$x-wK$P8o?D%6~KxNvI3ET*l>jvm* zoy(nHg~*ncmZdPc+dyq(l?#3w@_c>iot2L*;`y)c%9iOuv zT$`V;uRljKNt42&-Lw7-CEp5j>ztnjUW?w0kLMu^;y7){upbsTSEM$66`-~# z-*Z>EWApBJ7q2B58dAwM+xdN)>m<`3A3QcqUj3i7mYf`<a8A@>DBT(NfNsoKp5x26oxl%N5(#Dc|Mr$+IgU z-zJKlQy6|ZeOb8k15jo@;Gwn>VK`vt)?x2{jq%bECe$i>h%&SMa(}k+K{qBf%A;T+ zUPTo?p4ZD-FE28&Zk$$YG1yv!`S^~*(PZQ5Z+!YibT+e9l}an1M9~vYTHL0;VV6U z;{=oJzN}1^ZI+9~g5Ldx6Qj>ixCvGs_W0*fsR13~A@ z@&+sE_!mK?Qw%wTtE`PD59XOH^AAg$^vkcdN={~ekq79yQhoM>$GYeAl6Y)w*#Ino zDPN4O&tKD7{~8YQ*|#t3tai{nP&ue-#?=;~x2>;7V0tH!QMOoccRtS}sibP5Per`M zmWh>R&bzaSDJ^i}4#y9OSBsC#sbj#&X;>YV9+RAVs)UQEk>R$6mYkoQp-+DlX!MIB z0xUR>AJC2>rIY)U>WX1c56I6jkt$S^2t4@c)NQ9CQfu^jw~2va?W80&`|iB}$$pqk z-h+#HR~Gfuza!OG`z>`IY+oR>g-$5xDhTg>fGGM z>b7f%tT%Cd-73A-I~F~%7BDf;=ZxAI4tX7pO(vZ`>v76CYc-m^45-5QbneFn$y()> zfpYK?1RbHr;}Oo&Ka0Ck4a^3C8)2@Fk;&Poflg3AhkY<)yzy4Va=lpf`Jhj@8sK(* z20ESJr{yD+J7jCP`eTAUXbX%UG$88@@*O1`E^?}!(G>VT7schE>#YJU_)Ncz#YEw= z+oD(9|Jx3FwYMWlow(~P0B7fBS?WoigbNq$G}SL!AR+^ONTiq0Qm;xXxA;~H=veVU zmhd6xd6Wv=@AM`_^ZbhAT4K+>qKV>0Ad5xf!<}CTRn)f-HCCT1pe}_K#7n;?LJ79+ zVm0g@MjHlno*4jkj0j%~awWiW$6>zWND@iQeZxRw4$;-$hNEBVn^XS*aHZ>&X=(%W z;7YRgX-jDA#5c7*e)OWJXdCy$_+L_#bOV6H{-;Ds&cOUBFLuu-{&dXZMbof#D>aiT?ktZg>37**;Eo%j9+(ygx5p6%IBDB&oq`4$^2*>M#rq%02IQ?jJ_C|XnSHrQOG>!D5M6R{(UZ&QT_+lWmk9Lq zDKK$1*#q>awu(Y5xSdN%5Zo@bS<}J_Ja@)$*B*7%)7|U#3j4lZ`|T0YC+5%w`TP)V zQ1|zhZ~9F^9_)|FK1UE8$}cy-nE4`hWDmYKi9KM>foZCwg(v4FWco_;(+9AKB3}J zuLhL9erK%GB^NCMisIWh`uhOk>iTZxzfB16UN5iO07K|<=GP9#cA=Q{CYP!$l?5hx zk~(V1%@vCAlhvi58lE{SB_jNX4lTw`?rXK6VXnm`f_6*rzd~w|I zP0VXVq53OzLN|`sW zbVk5~+t!0W|FM$RcYp2GfGgWOnPefL%^SlSfqVEL2Wl;9`b2HH?UOw~)WvG5PF$Uu)Cy(1MoMi1 z4Hz&Z)C5O|(HPUk$$1hFgSuj*@5_+=HR0o;>?7#ex2hcz()#E6nU4Fzx(5%6Q{m{_ zCNfSs%U=fThPUI$<;RcouA8f~`;hZ(Zp0DHguwHc2kQrix}O>6==A%t4q+&KRtVRZ zh814vnc^HDtP$xQgNEem1*F6p;+K5@nf}EJ!@G;ofZi@osdobo-fX1lW}dkF&pS_N zrJO=+#}7)2bEwkeY%<{xEPkRtK>-d3*N4yB*QeW^6VEm>uj!_ZquZQ?ncE^Xf>|BT z{UAGg_2TJGWDmry#?z|C!!v7^emS+1(1KlM=b=Ze5`RpVE(P|v>i7f3qnSu0(Z%Q@ zg43Zl7gxI$=#{cAiT%oe3lUTA^&Z3~v8UHS63-=G9fxHzfV# zA+-F)D!hcB2}8uY^M3ljHwec?qfeFX1{6Zo_TuD!Y%9e{Yu)+rDP57^uu?0R1Pbzv z;Cr{G?1)Jpwm#v;lW4_?q_n0be#lKIb6oXAeswF23dYvGer z-_Hhn$FaqJmNd*n@;YXWMYyJ)3Y3Z0lq`D#9i@MZ6aoEMOp#r=n91azSmJKR(r7NR zlN1fK<`53GEmz|n#g8)xw<=b={N!%s+pTTL!yLyFQMa>msVJTCNws%!{}p)#vkrbJ z+UQidsJCmBFM;*CCH>w?fsp`Ax#WvtX#A$ZWAUM;XklU^?lQCstfpbq2*;pM{)9tO z7SP$q2NbQNgC^|kWQ{S3FxHRoyC#o{u#LIs8n@mIj10i@@rjw$xW>~NmL||zrH}6N zOhmkV9PAF#IA`Qf|0QJZtfUXGK}j>#tsISfeq1MvBJ2ZnZ5+>@EZPJ6#40f!Z2O zf##&=I(MmnF+JRv!Wrt=ngion9cAw-u7HE$U8Ai0Zsvt3axrs!)SaQ#pB1QXpj(x9 zBF=J~?g8~Nxxw$HU#AgW;y7fMdj;P5H{dsFKD^4Ur;E+Elc*tp^rd5JGy-66^38Lk zEYggptp00HW^Iov<|`ONVnjzif=!~m_HAsTe>p5aUZ!L2_1Z@Wfg0)P+y=L+#uTDE zWKI`x@mXq}gognTWqB+lJ{%YrnWvpjngjwjoP`bsikZ^8p-fSBMWCF9qM5?hKbJ=Q z-zx#jSh`vS8|FS2x>UXJJx3^P%bU7HNbq;P6vt&?5Haf=hAIQv#b8$EMS-PRb&fU+@_3T>nVVlazLwIOi#cn>&p_A%On zz%^fR|8iaM2A?|zs-Z)r4K%m$eW}5YQD$CiVgrxFN#}2z4ZYW2(o5ME3u=uC?CCv~ zalQI>mi}n^(m0ATuxx}{My(we1UJtmL7E69)p}-td33=%m1yElSRV|I425-Uw z1Zo=hjh74=J!kz5z=k&)nNA&lq#WOapZ}KjQh2wb%l!l_E^)`J1?)2`T$0wlk=1t# z$#cfb(y{n#VKQT#D-L2K)gPX&q#LcQa!Vax-iljoAIx?S+HY90JpODu?|B$R4XV#k z=Xj3YcE=FrN9MkJFU8Rbuug(LSns*qvS4~lCm1x|r*9G$?9o=Av#IkA_-k3y)8@|* z+JJF8V1nmCTl!IXUwz%b#*75rn`SHN)}td{nhNa3STjNr1NXeJdy?LLBQ^Y8u3;`U z{%M^Lv7>SB>2gmV|4qdYAWN1q1vCE7;*DETA9(&SX5S+j!N9L%nh$8d9)J52fK^n zYba(eHl#%Y!vZqhrA5UgyZh+qWzyIk$9F*N*_t$hy1yeuIpw5#tEm^(g|+S{StRFU zf}K+fOM1Jy%0jt>G*XU~$q%OQ7_R416e5igWe7^y=aN6z*eewE8V0`>e2VaRj~liM zS7(=kpN#lpOx&Z*3g!oA!%z&#mq2@Cx=>>rZI{! z)`@V!i(Z$Pc9w&-->CSeR0hBcinC}FsXrK(qq5EIaa0TAhU-Is6Y=!Xb@Rvh5ofi$ga@CTcnp&d^qkvWtR63@L%UazphqN!g9$z<}i&aqd-k`L_K`1UylF6-?M7d6 zIjcu%3a9ZZ@oF(|4Y|DhJRG;0B8Ef-zdv@8I$25zx@B`dC=e58SF!?gfxYh8iRLuO zGfi#Z*4%r3Mv!FxhY$RGymLcVjA%aAfVV_ES$D7`YSM#!d%J?Fb8;vP{69u8p8VsV z4*dK#WC|23gnY&ub1?McD}W+cRx711Gv~2*A6r5I68qwA8{d~y17m7Z>1E_}b^NPE z-#yBG-bKj<=R!eh{%Uj*XjJ;Bv%%#iSjDCIunvFk>GjXikub2e4^}w;w_&R9e!cXz zSaaX}`sWpw*VDnlKo&}6f#8ev4b|ol{y5Vy!4DcAMG3CGms;i9*~Z+*Xq&d?$M;cG zis0NgZaNel9O$qNrK%49yZ#cvZL=r6jFt5S>w*F9BDbvL=kDPFpxXFLe%J~Qa_vKV zM|(UlHRuiF_Njcx*V%+2pdcD$a-Vmm{43f{ep*vlZNUxQgO!T&;~8ekWwqdrv!kDY zZmumpI{0%w{LQo61a}rLOqVZ7k@QOVeq!!lRT5w6<7`{7G%e9yirUh8Z{d_dDEzv$ zJ%cf=ctHod-(>mT^=}`hhjmm9OF%vpB3#Drc5vn7M{;~%tyOxvy*V75k}i=u^>u>J zmfYMrJg3960j~bL{83}jI7_Kpda`nDmn36bqh2a`nX9l^Th}>@mL!I>CaL2#AWEK5@<(BfwqDW6hk?L-UJ@=bafbxwSQY4J3%<~sG$F#$O0ZYPnwABX-lXOc$?Nz; z2{POdA~T2V)s6DiFNaK17)U3T$h@zg)?_lT0<7k|u*7thCKB~^71@vZu?!zImj-@npe>#y(lHw)t!53|! zWOt;(qi6e#vi0L2=5~4NoRVW%CKVZ@v0;SXFZu}Nb`J?_DQqKerFuYU>vR_V4733k ztFrkQ=?3iyfy!xmwPS64il_|`3uC`FJjwQ<{%re!vX7WxYxBi;*AMak)%%TK=kCR! z;E5&+E<2Jfgind&dwgD0KkT+&YC*21z2~Y;)j&$&s$q(=YEg<3(SLjYO=_r8 zomxh=YnLQ8aGzFB$Al9mtc&EU{hvGG?}0rR2JE?#(yX!LPq5%*aT-DVq_HqBr$1vZ zKi#8hYr#MF$yu@2FME=NaJBI6+*K%=efFBvf#xzM^ZE9jqO{kRq!Z+_))2Ni!%5C>1QL3`cDM19^^&?ZU2RfwF;)kuLHQUB`MEtjMTq3{T!5-9R96X{^ai18ajXQ-$#OxoApx9ZA=(WVUv`OnP zFYibH44T-CrVXVUi`J;vjB{RxdH~T>eJU;sgGb+G%70_cac3zawwBLAKW14WFG>3} z7NpiVLqGeSxmAeglTY;!Or3dtwSuNpc>FW9`p-1cC=j#b%lpNW_*e#8(Ut{|kzcY2 z=M$r%Y}Ui(d|EdV#LY{v;7~{DW2s-*YI)V+U+5w$#TCi|@5?uN8+wO}y%v=j2 z<`tTR4sL$MiDAx8f(*5wK|`LFSn7((y|6hTWRQN;@Ar!I8|gZW5UwjmtL9!xrQ6TD z9l+EGxpfIeArlUR|9u8%C=sVf^^VSP8;wh9H^fle-B-*rl798mNvP=K@%+=Vpy>;5 zO#Un*BKL#4B&#!X^MfDY<*O=V-OLvfrZ$f2>{Sx=Or0xBcR`zgOU;g!zE=Cc;E z-J^^;?B0>qtb1%vil+1lD2;r!^{I5dTG@0)zKlHo|2)hai z%}{cR+~%{R)UK%}@+DgH)$a9{_fxuU%;6ExYD!5Sf2KCKBRUyTI27du=6Xt!K2W@8 z;rNQ(pVr#|+2suolI;$!J^UH8{ok8%^}Sr{n(@XH_c$omk!Dc^9#lTvd9-T;kw& zp_PAJP>WBTS}GxAE`}5^$-+>UH*qtLfxp04tz6c(rqieAc=HvgD+R6rVzWY^`T{6sA5mwD(~0=n_AV3P-O}(s$&)=b3tkfeayTsJCzG?_x4QA) znT&Rkz>~tlwdyo~{Bu6t(odxcL7$jc?H2AEwi5D=j+HPseE>65JQA+KqWM}<2uuEX z%*%vZ`Q-k1N$xt-Jbu+ZRe?CePLCvcxxk7p=Sb&`#8-^mcd~vG7-36e8u@O7qtRTU zxoa~kW6s`Qd+d}3ITBwBDV|XmeU!kkiTfyVz{YnF7|UxO9O#U}8u!+P^5-tSkFy}w z)J*j02q`(Pe%4r)0gV>kBtiFOWPFkY)MiA~jlxcOk%%!#85_}KD0}xJm`@mbMpfRg zQBzuWi_bC4D;M12a|6jXTWK&u&jwJ{WlOT`>AR~M9gYa+JAs7KmQ5WCuy*0H=rl&2 zbf*NbBr?)_q}6K=T^rUnh0M4ZLd97^<8|(P<0hVDyM8?M_9%V8oz>aj_`n=}Y-zZi z)-}5PxJ0dmr+N8|ig-S7YdE#xE#7$KNdvA$0?nb~)o+S0c~ohd=!?);M~C@P_Djez3Ht)2TD-pemIVb}%SR_-Y&q6tnhqY*U9A1QD#W#| zG^ulW?x>umiukJ|NLVoOIq5g0_P6-n^zcI9v!N}-#64!KrQ$g``vY*APrcf1&_%De z#YW>pxXd;3`eyy+XiJ>F<&gM2P!85nbxOoni0`;_K@;vQ0ILPRaG8Nf{c~^z*~Q*2 zjp>nw?=%0iG@^qnfH5)lvIsr9Gj7vftw+SL7ZE}C$no@=Hy>tevl$gD8mdG)MdrI9 z9S=t}?Ri)G1g!_@(|1a+!(AtdyZEGC-y(|B4#;H!Y#dA5d4MTUEozRoKr6)kN0ZEQzqBlYn}+Z z4u0k#OZ)I!+>G&4!g3zmti6pr3a_w5qLZvbJflQ4ED=vwuJed^;ag_(G%%cBisbt! z`O&O4*iwgG8>u=MhoUb0IQL)wO~Ah4yCop`<{tLby?Jf1^{~sLKuKj)pdSj`#@fOg zWi85k=&ZuoOyzGrrhvP^?V}WQ_fM-?FL?ket*yU5ero@^wu9O?;xKNeqHkff@&GhA z54_wKYC<4rY7hvzj`>#HDC6!+udDoFGwR+|d%`CO1ddNho8wpQJRVc5rP&c#w0zC6 zS9y)Lu4Dwp+WJ4mNJniiaPDc_+s<#;|C1hbdsI#J$WO+hlK9IP#@>Dt z*tH@l=R^fRe&)$jgfr8B*8mQ5f>@^5T%YcMG%Ho0^u-C|&JA(l=>7;3SXav=j&(l8 z^R~=oPFG{(Y%q)}@@Qeo zL2JUa$*E14p)Py6OY?iKJ5TP;G0@F5hRl|5uv%lZizn(}2;@jma}E?|q;X@~FCl%A>HNt6cCT{*LJ z{M&yM=0ag`=M8Lay7dJFpNc1vN@+8ik}O9%OJ3p~&G>^CrY<8!bCgVlFA6uoUXVzp zwiq*hH{Cib=p*p|NHZODTepYEK^AZ?EuOJZ)j4p8oX>M+36?vTbn)v@K9`weNi?w(+W^@|b5~H0l z)_IpzwfAg14pr;c!u(GyahnVVEDv?tUmI`#XLp^<2qE<9!>vvHc&)vk(700Q{K5F|=K^)r4DHDo<*pRgy zf|Tkmg@3XDP7(x|SnmN+F>=0tC4-*Lm25)M%VD2T zjzPF5nt7}XT!kw*5pP=h8D*WuA+pb`f&&#To|KSPR&C=5X|V~;f0vOEYWINr?a=Z4M8Q1moNM*;lMI|UlZ zx3wPvEuf^PiK9j1YClS@5DGgJqsfohkENV~Bo~b_pFLX|b!kemrB#uXx$^&o1;XX2 zWF;2-Zo}vNMkv(tnXY?VPgq`#HZLYgaxJiX3>dAwXO~rr^3979*j)DLZ&a1n;x+A5 zd{EFQ{HKn2)k^vQQojCb0mg`Kmj{tAjXkj*2;{zDSv8(tFs@b)QCp5J2a*!~tW^T) zCP_fdVMvn7Ke2FartGx~TS8i&b%iQ;y(Fi4C){ugQcYqdPTjpr~0;yIvx zN$lmQ(4jfw5asQ`H-Dif&zmJY%5?X?f9J?bZ=F9(1E;%DK8b)}R?z?O-P%gE=QxZH8f2U-lg}r)(c~j7b}|q zqqgyG63N^P;b)w#=0RR7fl+GkrVed|B+PL|8>)iym=h2n%SC5=G zzx(a45N)H*6Gtt}EarYcVqHH{)f6A(_L1c3D%{t#^r4gwrsJL1p;b8^Egq}a6#QOq z-PvOwPn`dl$bno02^`*6LSg|NSP$1UcSH zf{qzq#jSF~gAj_!@ZXBcNWUDnE))4CqLdKFOXTu}T+*wD$0J+E(4{RaK4*|Da#CD6 zZdQhzEbp$nVwrd&*duN|)|LCp_6x{-3qa8{Qi&VPR`D$y{ZKK2V6Zi2iq1BxOx45l zxseU{mfxFu)kx~*`b6_zX*a>LVRI9W*W_OnHf5a@9SGm>+X-6-JAC|d~I!%XE)^=s(5vpv#9w7OSV z^{J!>fenWHV=P@kkEetmfTE+AJ0p^q3pB6&_pC5(mGU$y#FC*Muc)_>V(aL8O@h&D z_M|5Yr!)jxfi(M%GBz}h&%$;S1c1t8q}{0ah~%s!mZS^*o)=5eU#g>$xkWS_%I5EU zl?xmBO8og9pghDLo{!CV<73#+Vf{X4QkHym&$BlnI<3XqLM9gwor7slmEwhav>&bG zytGgfQN{23;YT~cDI2V(+21XX5zY^j%ed|)h zsetzH~hxWsGP0N=TsJCeb?s1hzsc#a0UP@5Zu3?F|^*8=lxp>(| zY^a>N+B4aC-AF7fPhGaL)n9N=PFL^ymtVdURCk?#q=`^SqO>iu?vvo>_D(?*rSnxr zkU@eZ`o|b=+#+dDZw0*);Id>zu4CHtZ#me5CX z|Nq74TY5UlAnIM>nzjI)N>~^9@!(;0uM(BN%@aS&HEcq$ z-J{sIg@ULtvTA8k&!Mn8>0q&|U*HRw`MAHo(~ha3j{BUiL-USK`f#>!?-@<}R@1Je zbsxjZOTVM-r*ByRsy_A~$)+`y7&$fT>eK6;#K@*yA=-EB)8bJU1@t&(?5m5(O5wjp z(&d*#Kz0)sGC@G5t@g1|)qk&$Uio-pAW5nX8D|h36~<*e`YEgc^0O?Y+HQfHwSu2s z-XRWCuU%4xB%#wuXZsBcDYZgGX2SCl=w-f1F?vf7B%o)TfI0b^KrhL=^EzcY;;Z_h z?xpH^66{z+IA{PZp8EG#Ah|j*XFW0!ON1q_eP=yCcpHzKON|SkZd=sfnzw0AQJE&= zJ?|UTtzQP6 z<7Mw5Yj-TD_u}J}dS{38phdsoEjsizf7a7Fw$k})8-EJR$i0oFcBJI* zT3H`F9u@u1zZ)n{32v(o0u!KlthKN>Vdu0wX;cx{HpbMA5KM?aex7Sy7A>x;n(o9* zp<~fw9-~V$R}g|uC`WQqFR`dIUGS(Ju*J0wB*W%#F?at`rZR$ng@Vl+SDcR94h<*S z@4L$l7o@B2En=s|6kml-bj9<{D{>PoDL^hlmC!)3oi?1h< zBz_R$&IX$p{q_b(MUVh8nQ|-Xa_s}^e!f276YmnjlXW3R;)6eYyJ19AiiMO)Aq>FT zW0dv4rsDwv4~_g+10SRxDWoTno|5{Wq|`>Gg7KxWnooGyo-h;^$d z>B#etGQUdp?E_+vFne7#tBN zA>0T9TJIcid`3}zP@BmrhQ|+|U%pT8za*!Scg8KI077&i=PSnB33OZ>33GfPJx_O> z5m64dE#(ch{tj})MXJLlu+zY@LbAxywh~O`&Z^kTrLr~=uI-$y@!YN;^3alnPYbHD z*m9RIi|Vph3YzD0pDFc~Z%PC30R{K;qyIg7a&Ft+9I0qgyc*)&*2|xe02Q%acV9Ix z=sZv1&XFHm={8;haP%nW0Re(u`%vrwOI;{^N1P3dWftQonem2h!%0}f>bD(-dUPAx z1Q410pyPIfft#%s8*mN`rj|$^9NiVDXwc=lDhQ!jHl(Rccn&hMwPloDRz})LW2X`o z+;a>XU7fHu6>4LxubtfCH6i#_il{~b;BA-_l?>&--EVOw@US=TFmPt}e=mGsh|I-O zx_q?Z2UB8g6W5Mh51hO_U{XP6YxZqfEV{?%W#eEraOwz`^^6IyqH4G)b58#kNPtL@|PXjsG1m@Dh9vqZ0lx4l{?8Ciqg~5 zjp`p%wb+R75cMfudY&npW3r;#!u-ziU$`#jk3C)hflu2E+vHx8!?(jJsm!0gf{@CE z%vBE-=b=kl{9npyVtbnon?lHcQVspN%jO4D!2hmtE<5`+wBrFo9144W`5b|x<`%dn z`M`X4aW^sOgg0C38fph+)&DuR==xD9{0X3u^hcchBTxPMX0tH2J#o+js=WZ66W zIs>gmS8K%%^^kmECsMP z;V#L-mU5whXC37YVO;}N93qNqNOgsKK>SOt!aX~?ZLL~vLQa6q$NliU_$-ZQg#Xr= z0lmD7nZK0_>luZVxF^3(Al=Bj@cXVUEY6ot{~JfRWh%o2A%>*1Nk(9+5FEnqc*DcO z-$^pa?IiGcJarGAsK_+S)6T7lx+AwzyciY8ERjO623ND^=8vT^Z+A6*TBrB}()s^b zdkdhr((hXoO$Y=J?hrz72o_vIAh<(-00DwqaCZyAl3>9acMlreNpONRZXIZ#u?8A% zc!!za{AX^}y}viN-m5y*MK{vTIs03C?X}lFU)1J%2Z!(?baFfr35GM_h(`uRdG(YG zF4*@^!okOekUDV!_!$kn|xwKzord!i9rvS#rwU?^O zmb*lf-4?lgsJNx82+sAMj>B_U5ugxp9h1~)Gm+vQjKmP&qNGySE6PKUrQ`9=$GsY7 zY>tx!*IM?_DH#lX7hOuqfFl>5b~|xHC#-5{I{a@&@SGNCsL??wj)h$F=o-ZnW1$4zq=SY++$WeK$(;XT(a3-1Xy%E_UY^ z{~5|(FDX8CpBKmv!*uiBxAl{KRFY0gc}{2W>Ez@-7l$6b-re0MA?D+xzql9KAQ5!47(0P<4hR>hGZhIwjN|G^I%?8mX-ae^d|jYJ{16Phq{-uEeJA z?GLm?8}v=AvLdrx^R2_|4?!|BkBIgS>&JFrA1hIaHF^Y4T*UBza5=yy2o359(QKPsdS zL}5-DX?)Qb=V90pE2O{dUj1_h!qd{}KR!CgpZe`68K&TTAC=%aJ=VKoEr>q#*g^=@ z^(mCNLA`}$0sqJDY{Sf`A@=S15NLQ5F~?moJ2&Jk@~b9VWyLM?eaG+AkS|p-ZuZuo zn;?Vri$X3>+R^cw5{?)>mH`MFDgg>`1beZdnEJJLbbCqs@%)ch&&`PeeSk}m+ytZ) zm}V}ihK&p|)Nw=47;D^x>X=qop|07ik*awO1AUXh0LM)ybZTBbcl1!*k`fGWZR1bE zkMd|zh${8gIJYXDGo+kcI-eNX(}%cz7C%i^!7q;h3H; zp6yb-2EDN$ZxrxdSoO)7`E=QkT|&168$D2}^(xRI$vuS6;2Y`Nn>%45zqgPmF4#Cw zIY`Edr8KsOE}S)ec1;>c--qP-(MnvOQRM6wyEBO(iG9LnLE9%Qda`Nb^XSkIZzOqI z`SUt5uG`{{FIgFv5h=QCX)Bq9iF6#U=p(SaQn@ukT3T(%`N7<)#VRX-1b?OlK_A;} zA-}d$c!(H?NZN7eW&Jl*l6~CCIpb&+X-07blUvWX(1*$L)>P_~>2bsmbd)X(GjLs(7$=)9;&3hamm!N6*bX&hEk0)_Q-zN z)SAp?Ah8pX*JC5`P(JY8fmdXwk;SA2!q(&`XEQ&2Z6gD)Qjy1gL%k`2Fpe4f! zLb*G7MNzRJ5F|B+&2o|0ZklE0IylZRXr#i2^k%ic!NQAQ zh%-MxAE!C|9nIQM*<0i%E9_L&HFX3DKGH_F?;>m(f5U__>kw@pM%>hD{eS_Vu4mj( zrQ4tT)hgN|0(0tYrq+pXVdwA{lb^+l?mW(_HwgcPiw?1Cm|*Y(>04rZSP9J$`BCLz zllW5hkDw4<4wiFRYYbvq%;R)?rq;TVDmFg$Tlmy$3neUW*@w@bVc`-Mr4zoe$Gt0! z#=>Tf?4;C)3HF<6_ReG*5%o=E(|B`(z5F>ovVwp#o7#gxP;NfE)4$2Mr*AH6dbZuM z0l~?#863FOdeG7#dG8TC@``TUrKUm=_@M3%RO)TRPq^cG=sjo}Vsm`XAxYg#b(UVJ zD_1WUZN;x$c81d5I@B~S+atd&BhsRFsd`K%SxF1yRcdoXo;4JL*#5r^OZ8*QYr%Q5IDoL!};}nYXC%86!Ap15>Qzcc|W6 zIb=3au>?Y3-_gPkRX<$5?w(BH7Zt=4*FGa?$?PWlZPRs3PtGB4_lrf&dWfnRwmj@oG9*Qe$ zuPtMl*eZw8b0zDoUYeKg+)HW~A!fkQ@Lmo>ktk?dibBWnULDuuBfmX$d=&xPc9DwJ>Ed$lpkI%P@UK;*57&AKz3TBs_y-Wj@Jo z_o2P=32(?reRf*pOFMfo9^xD?TK!r-rn$Yc>^9)VOxdu8aUA&*tkN>rz^C1abGjAp z_f^$PH|=(YLf@~&x$!RskNww*n+az&+Sib-k&(XPTQ{>`4M%8{P*;fjL#ub`Ur;QMn{L|*wm4>xAH0Z1{m;6+Tc=W(52VOV zq=R1FM^(yJJHDA}@s^|*JPRy^Vl6okh{h}hK&&4A`r#R{t9Ki<(regqGoy~CoK{(s zFz49sK?{4o8z(Iir*0g(h|hS!u|cC|z}KxB0%X8dl*c?bm-DvM9+9P9LnPDVU*E8L8{{4(~DefhYiz1#Wdz%YYyf*CQdh;IyBxnQz zI({LA6#w2|h=_uAALFi4oR>5TtQ?`cK7`KxDwSWww*zi}c$K5Vs7cisvL2bUjoI?G}69=OA2yEjFX4o{LCY zmK*kJEC^enKgQ|Ds2ngIv9 zgoOX0iIa7rP&eg152diY#{E4z^VSBVMo=aBC( zkCi!%SCM3g1>YSTp*SBjMShSNGrwz-20Q>A($k^F%ZC(sl`9eHN9`M5&nl3YGYojlETN;pa(~*}vzj!O{ zuk~|fWw38We_m-<7iCNilF+{Zc7Hr_8MCnYL#X^sm!XN`j?^A>`s+}C4a1h`w97nM zNy0jrC8W+Pnt~IH`UA$jj1;*F!zbR7XCAlfYT}>l@oiT=`7_ty9`d0tTb{3vwd4zs zdfM*}R=tR!Uykxbe&MFe*!Ce>A3DZbcQ=^w{+^gB*SEbak8`xubMgHN74BcTisdd> z9lQ5w{UcWe%lrvG%M$p3wvgLd5m~P%S%ZWI(na->9;tVBvWmv+R+U+v}1@L z9$~3HeD$>NHxiibAs#zeIrTSR(6sell8!GzO4E*^%e$8GtoZ0iJ9z=wDhczf&0VYv z4s6pxLNVBrvRZXe5lsw1+VlK}c>X~&i^+j&!`_cSN54uyK7K=Lt5a5caWA2xM=QnV zH-uIt#wzvNN zbRR8{8m(e7!CI;#%~pCD*%QET0oHHe4J7zU7D#VpI7t#uk}oPqRn#+LLa;Li?k#X! zo~^!Y7hBeI=N}sr7Xr^_dM)Xv>b%mj+qXOs@1(vO$^SD2473Q*94ObwmEhC56O>F3g$gC z4B2naFn;A#w~lmo}Dx@k%@|P&~ z;L&Af0Eb`3YW2oIby{}PYgJHpAyr+5wNv9N3Fef}6VWnk9zC~zJvbSzqj%2=ZMrer0d}V7QTIOGav=z# zN_{?JPPco==NLXNx%&G$JAu=F;oFQZ{06bpv4Zs|Zef1ne3pF7rxj7r!dM~NcHyrM zFR`cHzj_jp)PfUpcvI23(t+4fG?JPcc<}SoAAvEG@?rM7*UIP(_ds^4QWaI=hWAwo z8UrB+bG7&U*99ZmKL`!I2S9%ZMzKHB6C`!c?oYnyAZblc?pRA0n)jxc+ncbQ_C!}{ zb(p$Ow=Rgf{JbXJiKz`?fk#zkC^Ry!qyFP8YlDV+zC<-zkEQ>v&Ut?LAf}_e;Yym? ziR|ia)+*6;w3Bg8Gp{oQ8|o0&n4PD~|o7Na}O_Cx}P`Ai|VMUL}e7D+^~kzDSX=Ns4AC?mA39jpkm4>w{H}?9lVC9BQ6ev zW;O66r1d?Lf?;P|o%iHyb%LLP&n&_^vB)ozi9h7{`Gw5T@c5q-Uy?rRqH#DMr^eL_3t`y&bjk2I&v(gv7SZJJ-tyCQ zB;Pyt&yj^+FyGENL`pmSHZWE@#IDa?=%SVRneKGiWpN?_nR9BDaUSYwK-DqlxnMcX zY;werw>2-_zX`CwJZ^|(SiVtx>-7AerXIG8JimWJXo4Q&UBu_DA>Nturc4n)X7Lttx-wT{hcSpf_QDm|HNmz#O zRv!fknl$~hV2P0{Ehof~Fx;s9g&zDmliK7OmqBPJ#bnhHy?3u)*mBzW@{JV!$$je4 z@yQAI4EL{IT1&Bf0+}Lgofi_xMXUo}8iHYPJS#VVudwi_XOd@q1{H&Hb45c(q4-ZO z*6|+|OvlFb=B|)k|3O+Z*b9Bm*nWX07#{_*fs_AA0XEejDF; zL)6d0!ovRb)bsZ_>Z&s4qmV4bjd(;j!_k6>e^lviq#k@_VBO z@9@xvONq7NdkCBC=);c<#}-`6ie>Hk@)s8upK&_T|He>ybxy29{9V#VyA|WLGUO%< zey{GKssz1+x1}Sr&=D0>foGOnH`)E3b2T;_RN@M}&Z=FL=g~;)St@8uEnfid@;;l`Iuj(Dl95svzG<1Y>cG5pEOtdkR&bH} zz=_WfPWSda&!V9^7IwrO+iX)dzY^2a#H@OTe)N0=!?ubxT45<>Xt+o|Zp5wiSk=PR z?@1W^sC)HKP70Lh?mJTjGZhQt^YwPtXZuDQm0A>0S4!k2ExHb+IkM_af7d!F7^OnN zy}hy*yG6yt#Xg~(lcE1pHltt>+|GTvePey=Nbu@^`4w)|8`rhb%sCP?fOmx-GEiXf z2czz>sLSnf9fcyy9uO89%D>BLt?XfGx;h#e|J+{7ql4MxZr`eMjJ?4;{@e{R^B_vg zvBa)t@cHPVp|q=@_{L8;GJvQ{vD^`LkbLnEye^B^E~8Xm3G31a0~mbYF|^(g#GG+f zy!AYGW2iD=oQgH18JXMxEl!WrFSM7~`|Vn3h-Fd6<6uYui!sH-FEdThL2a zpWWpxrr4yhPi7jwjUspZH3t%T;L4`{poG5K2`j_R%%s`ooP2$fv$nR@W}JlV9;w8} z@5!y^oSvwTwFWXANS1%VGbFGnT06sr8Sfv#@;~~>|Mm+4@EuIRQpoW2 zbTIlovMdWVyVde^tU_HiTZ}%C45uf*Ab#BUSoph{c0o*#c(3D-aENbZ&xGLU_SP#a z8U_!2c6KS@Dzv>^nqzCzQ!#OheIWfiOTWz);g!g<&da8$^J}$)|nQ9C2Td$snaGQS$|1 z*s$wXKb71vZLr!QZ=WWp0m;wRQnw_eoyz;aS3BLlMDIta&4bR`Y7ijLY(a7&o(4o&I!tys3-I zuBizYZqZ{_b{vHv(E#7ccY}k5mC$Y)AR4%^>QvQoQIWQ#R%^Jj9w6*Sjt@eV?lv@Q z%_RpU!+f%*N`Ad!aqG{Grb`@-XUTTXE+3Bt61DZZYPiRyH5qxn2ZTSiGvSmHN(ifnhsL5O`LTN2yU zEj4TeT_<#=JQRyRIzB#Q=fr}Xu?PHLgpm3JAcU$gil6GYVVyWGXZZ-his(Kn*4TZl{`oKLUNP8@Qk6@hgqc<6hle z@W{TsM$}si12AMwC?_Sw_6`ZGDAO!1f3Jw?~g2 zsXIH@R9Tyu<$j2}7~}Fi+jsZNVd*~xTNT9_Sub635YmVSYlv`IZe}Ev>8cwWJ0`x` zTNi$2P+v$hCu$8H-PME$XX}(bWaHq-kEIeyq#UFEHEBrLXg5>taqdOT*^%4L$fjKs zdpzBGA>HjTlsvLLUhHqIXC}i~G4h630B>{)gX?+-h0?E2ATd4t1-_UU8I7x(lgQcP zO>p-)tdfSS{mU1#=A#`Y8`#WB<~m1p*7>2&}Yzcs>l%zmbUxpbhp-3K>LDXLh`huC`0;Z{WOp@L%EaL?eqR<`Qx1CoB;@{bP zm>I{pRWdrM-(gb$p8Y-ndk9}>a;b~{_J{B__&MqkAcsqe1P3F3J$;c$?c|B)*|r~7 z6v2s4MKf+LdtU zd{KK&7~GB_M(_RmJu9?r+g^lhb4q?);1O)LMj?1pWr#$43@ z`se+Vonaf**2W&Q&DyaWJ^{P=;h$^7tyy1Bu>29zDa-2Hjn)%i_FK*fO|C1AL~SPY zaIAl6#YglB>9`NXq)E|;dK6Ag@x#ej39r;7Z(n&Xxu-K(H%1N6PNqi~358G!Ihv`I za^@tl>y0~jE-DKP3&$ZF3=eW4apE9S?i?Fo~S3*5p5P56FwQy_euZma$N{=+L& z^wawl(NO)mEv2;cS!A$`-)Ij>YNx&x=!HDB^COiEua4GA-D_GmGhkOR)uglDZyO!I zM$984N&M~C-yw|m6+d5_K1i|-;%O&Z%jN$E9sKV&*BQWo0%VS^I0Z;l=Pa#ajD`EA z?pH4S*FOPaTG-nlIYKzZ3k0sc*AShx=ck{QP;4)low1jjNzjjDxF|)B_n=NU_Z$_< zT4N=A7MU%^Rj{#bLH3Q$>?c{UU(+X`1qkLVj#Z^N@iY6K^G1%A+nl!~-PwDGxI(+A zPfeo;C^&+VFITrbiqvmGFN&or`mgLCh`KcVYFSY&s?zm0SW5%Ee=qR13uWQ)Q6GFK z3uE1ov3f*V1Y>&Ytwypo-RIriXo{*clGLBFvY=QK!nf0@+>_}}ZH`I`z5%z?YHZHe9XH4t2y}8b%hxi3Nx}-%UsIE4x?Eq$Y}pSckcA>CAdpnpA(p-~Rl*)X z+Iw7bcHM*GXHhn}Sw-$r!}R9B`yF>P%nAQvyb~s-FSCR~X89O+lQVs5$BrvO0th-# z-%-8owgAsS;-fgQ=*$km`2HA_;78)R;c$uq>9T$Hv9H^G#ArNSKQEjlSi9Xis)f*9R;d?6jT5Gs-j?N zV**L5Eachd2Rk%~kFqBQv|LCJ{ldhv{^jK^9DC9|$b+KAT@>^2Qbkez=@Vn2xbYrW1l0C(2MzHuqDewATe~6VkHG?fL&W~F|+${Ty+r7h&%=jca zJ}oijL&e+y{2=W-ZXc+qiOzD``Ni_nj-gGo2re98d>>!AteL$wktcvO3?752H%y%z@-l2h=D>6(-;4btv^fX9{9Tc_ z-*qm zJ<|UDpk0Am%@@?-qT;PsQ8v=m8*;vID-suQvB6T4y`t*;`V!Awv!FzC&xNC@Yn;p;VhHq&Ys0*1`LRf)pDLd9ZDcME6#rfxaYsj z5!q~5fYeQr$Sc1{^rLtwoXbY2ODs#ivl1hXqniJjhi)&7eDdhkb=*>7H3iYWu=BDN zw7c1uL&m$rzR*+mH%BK|z8}B`ck0e}y`Nz^0x2D?@E<<-#a@&{7O=!G44yuE+vuDV zP}qA#XTtH?^YR&KtsNvvX4gzZ2=aL*Ec`bGLo42tL;{p#UCBT7rDTesp5)e6)Iuy7 zE|xZDa8003;=I6~rcFZS{1LnOW!EhB^cnUN8CQlHF`c&}Mi&PgoktJBe2ZsQRlCs5 zbn2vE`>!xK2P3WD2J_D2Q+pO}{it}W_R9vWQOVnN$&(+wSG}GGOZby(k!SfU0fsMy zVySm}T_e>@0{u7N5FV(3j>!G5qi$S>^Wv2YM>AVK@?>J}EW@VSt;QQ~uMAYr8w8fW zB0yncH@|yMwu`d7dTc_7#OVrqFC>xIOyY;DM(brpWr7I3qS?Tki=sL~*I4d={S0px z%A30Fu?bPv6{0LX^1uURU_n*;(Jes_z~dB`TIf_48XQQtGJ3}>!2*umSeFQ}iQjQd zlMP2^z`TB)9awdp4!ct)0PS-ad>Q<<`jQO2!C{3|P+{mq_hgrEvTszm7K~Z?_PE;oG+TNrgswj+o?f`2sDVV#6SH_2; zEWJjQhRSMZ``xG~wVBIg`I6jDji2=+uYosuos7*{j0=E}b7r3EETaA*ma_Y9 z9^}*9w|L#Dml&Q#;Xl83dl-RXp8gX@O&?EPG22hX3ddef1#C|VNE0!2aU_9deE1S& zPu0B5$$-wsQ}acsY_w{WglNs|D!;4;`vbK10OX18q@tXK+xt`uTYj2Frnj1M5NsS>4H<8*d`N(zw=f)4ZK zx>e4QC;Z5bIkS#;tKLUd6k(V;pHaKd+0bHb{_Qdc%Ig?_3hG2q68A9$FLT5=^_x9$ zO=8nN!&cHSqTqN7zMOom8Uyv#!8M$jaZQGhET+F+s9sMvW*b*E;0q~R1LT?y=6?vs zK|}+3ZXNHBNZ6M>&auULK{UI^m+sUnNDtC)wJ+CILL|qqmXb8glu?(UE%HytB;3C< z`ni5-!%i15d;sBNhyjPauTn%)dm|XBnV`k)FnHbVM~R#_Vbfm0UTS+O@%4;W{CHQ( zIAO98zCqZ3yV4<%ub?KrGxd!|0yR@jhWqJLdUnmg+XKH4(ZEdkj^yi0zSt}t%@8k1 zA9#4`RJn~SOG$C@2i@C-FHh70VstBvw`T`pKvG2+em<+)N2=;l&~HkSKcee&Pu@-i&oa)Q5JPUDREFKd_#-jqRyQ*4mPfrB?H6_U8a^gmSy6cE&T)^8T^< zs1f_3bU!;@9j?CcoCx9WIAkfE$cc0-ilp@mIX98s3hdH1y(#4fur;ZU_b*St|Eb*k z`*kTD0)Wo3JvEPOV&=(4awclD)G{f1hV7q5waK_oR8e8J)TifgW{(hAv(0*m$Gl#d zavi!jUc8ihS~a+R{Fz-az(wW!XT|Bn1W@hv-bdw(sVbq3I3wYFTJ?%uQ^&pbi;iPk zh+E-i9Ma$8mDv4~p;+@WlPxa9w8}d3r!&zSpQHR3N*K-OJona`vdtb$?~}FbHx8XN z7^R;&FT|5RG%tiF2|`#T+f-aTGL3UCb+>2@e&5{R&kv6#4NGHpGxK|FdQDN3r6Ppe zF-Hg#SjWBx0p**j^b!u@LPAI21J9Mf7r?f*)lQjyl-DFFCKVV*Bl!{T0XKbtTc4xO ze!jnM65R@esBtCm+|)!RlxP)IHc;;^w?#KNUS&Sx1OL(xk1wpNOZ$z?LG!(PH$O#1 z?6%{E8$I8M_@;=N@{8Cf#qKm!GkxGVnuUT`H!AVXdzb#OuEoY} zmnWCyde$?$dfjZ9f970yO^;JNO1GCA{;`oN)Ng}$Bv7!G|I^?2-|8v<>x=jK0se)* zm4Y9=vVsBRQRxSemST&iUB!8ddPef+`@BbH6+uh9fSIK#>s2NR_qwuOgSQ7!L#@|j zs(g|6PD_ZV^PG%W=40JfqdHew&cz%|eGi|zA{of2cSSOlO9TG&XaA>lI`P~K%Ee1@ zxU_oXl@ABW9>3PJgy5V;FJLdLNRNIt`9jU9HlPspQD7}N#9M+Bq!v4XiKe5H?O`iU z>2u^T1&*CiigJm}Ihmf_q(;lg-35jcY@TK8o?(iR$7pL6|`_TCT5rG$(A$5Qs9AXJsH`g zHxj`Lw*tXgC<7qmgF{0F(Oeg`obe4rShB?ZYCh}ww%|&1T?A-J|JP%rjuKutZ$b>vpp!5&A=U7M^MomBCkPCmaa{zn78u_7EP1R=tc$ zwp#imZhvUaLveFM>8r)(`|yBb&SkNhr|6CIuoAn6%VJcunhzjFQsh-8D2rtoQZAa< z2zy^fnz;siW|l&2ubb|5G=t_f`Ahq4$&%_HB$g?S)g*u9#=DYuN+b_gCG|SwGEF6s zcu1eaQXI!t?`QcLC+b*c8m}lkpOP?M8#Rn(H z+|k=L19bUXS0L#;xi~!#5n+vJAeT02K*aZhLww37+x+@eJ3onsgL3p~ zJOSspdE3K$W1mBq|smJnID&LD*9pA}_j#g3tp(7Z;FeqY~2s-}7*6K8i93U(|4#?-KqBn}~<3RJi z$)TrO`)$*B&b(Zbm-6Gdn$^IFa9o~fP^n>lU?eP$#Ziba7ZsJ@kO+&UzxE(eOSOnM z@5!s#^}dx!2hSLH&C2E@5s{ADm(Om-0&mn*-PB$w=ZoI@(AA4Jh0b1_Tx1hN=Ds@` z6=qO&uvp<26c+x<^jwtpa}hBKY=Hq=$$PH6y3HrP`Zb&GlpqnkLg&AW3LSe#X5vO+ zB(bFkS4q8m?tcQm|0_$>ik6wBNL_cvpxN|%TJ`!<7kJ!+i=3Vpyy8`GWTkiu+?FuU zQYC$8``mmW_Py*DM|B6Xp+!d-ZN$%kvJJjz=dvyyopa&iJ@3kYX0&8Aft>outr<L^`qO9#FP>sXY3$#$^)MN<~|Cg=T$R! z5KPdoho+r~@uyMrUp&rM0W^E1>YVo?SeZUgm5g#+6{q91=P2Xw4;e4^#74DY88vr> zyfJ4m%b4%pF4(;NYR51~6udgr|FRlJ4jL_gL6L8aIj>+R_ey^eV1MgbL0FJ0R-pUg ziPv@<_F!htzSI?r{VP^Kwy(kx8pr+`T;rodO(;C}Tb`(U2%}YUC(5hXx0wdhwDsNx zQLHcmETVqbA3q4qx9haVzHEW5+GJ`iI~b9cBY3$`Q0fyjH(oDCza#ISh*%~Nc~(k_ zl8uc3b7=UIJ8VYBADMJHx+wJW-2tn-9v+1vSw>(DZ_+rRbd>0h7$~Zb`BMChf`cE@ zyt|eKGh)# zFi5;*`>2adnB|vYm&g^MS3l+ER@+&ydh7d%2ZXuLkqu>MVUZ7@r)OnV=m%osqa&rT z_T_0m%x6116^|Xn&LR{^DQI0R!(Vkp*OWlYW$;#OJUJAorNvT?Zw}Goad2?_4z6q_ zvseOF_78A?E&&qzZNT3q-#hp3e>sBv?_A9ODvuDQxWUUn*A=iapBx*Qir@$)eO&sn zK-1M6z7BdkMoP>a+c-umfLARFOig6(U2GnW5kDid%Wd+&oG))B;kTO9+F#rPnjyX7 z!OlIl(NdX>qNH&PGy5za)bnyjd_2n$zf<(SRz9?cC=D-CMw67wA4hJ&qmDmg9yfof zuyb9yY!-4qR6Kk9)JOW|x@!1(loG2|U3S0l{UI6vq zlE%NLJLPY%Cr5Q?6>+(ptRmj=1-lFgE5J##6mG-nEA0sWwXoE%=t5A!kU&-B7?&XGMA5H(W7l(0X-8nzj?7S zD{j)CfoDsNZf9+cT7VKSpac}UqLiYWy0!(N$&b?)-g*_r3V@JO(R=V+)vv;!ek*DK zRCsct8y72z(|(;X-(XX;%ef7RWfOd3JCY_#9xx-f#hQW1QXTfKNV*O!mOYc=Tm-0> z(Qeawz+_-zxy{!f;We%~;tb;q)PfDF!+v!KG#uMp$BX%N_|W=)lUR746d!~H#IbZ0E;;U81uN?3hdq*wb4!m*N3V^*J?^qc(H~X)#@Xs&_R%8TvG|a&- z=gwKiDyG!RNnqcM(j5j-exP&(VvPOmaM5ph5j6+~!5eB(6x|D($wUSrQ$-d;LN#qyLE>{h0Ayimu$elcq{5Ehcn3 zVU1a4scCuw)ZG8pUu#*t`@Ei@pK8%s>TMQ5u~AtA);PFgj7>{ig0Kzxxorzvpk&<> z;m*5pq;M>c5KH?uV?mVl&3Ha3Elsg4P9%Az5*a&VS;@-z=6b0a@#z$rMOG#m`aN(N zTg5W?dM%PhA^hdb6G;5ZRqyfg>nfgr-&>|GwqlZJfTH@6;~+M6=3u;?Sit627@(U6 zpZ?yBBE>dgCbXW&Wv;hbiU=^QO%rrnR9C1?tl|iRZ$KhA{{1zPb zG1d;#`&s{GtLcG5iJbVEtUBpX)y=u5!NnM7MDD1$D7(}`#{5?i5Lnp{)-hOUVp$0u zR4=NItA*$6F*1KNDei^eHM~plgC*nv<8Yhi3I0~KnOHxP(JX__G#py)=Xo_h`p(Ey z)EDvkOu6#Gd;3DjhicWkr*IcfwX|DnXgV@Dalqn+y=$lSke5z$Kk)PydpiXBvnjrGtv5XWL_hl7t|?mN-PG?2rH$oX$| zQp09Tnv@y(D4#fM1y;pH+C78g1Bbeu&({Y+@xn62HStdqmR2@HO&~!m2 zNWC&=Y(#r)=+l?cr=*isja&t}X#OG3t}HUyk}XO<_d7YIDeHR=iSy;$F`5GmgR;r> z`zK{3w6(b(z1miIxaB`Nt_qt^PB0>}pPwE|6mYY|dvaMkY+QIQq~`NPcj#!kdAGF? zMd;e@r7odLYP}ZOpC_X<=uYN9q0k}-gL@JGU0DCWiRZmkKzKm*NcxVoDS~U3ic-HP zzS4#*ZeVz>pp^$1U32h(8I?!}SqdJH!#(Q@df(-BYPWx3bXUBNlTlPvI?1FR6X26v zkRDoI;$T!UiYiu())ujRW9TutycB-Y6AdfYZxor@tm`*hNJ^5ArqyTm|6YQ5^1Tdw z+WyUph!yd+w~Y^-)&i-MQ#sRF{l~Cf*BlSg z({qZnXF~;;+Xg(`g6H^;HxoUyHdkvY zC%dey39onJ1B!Hx15&ON=(c4Q1c;*4!fwZ!3PwdlPwiA4RHxzT?F$OgMHWCwG6QgF zLE{hig1s2-EE<~{C!?DZdKdj1AE3%@LpEz7U=Ee)eUe{Ft*omv6C`e&PDZSbJ9aGz z=GgF#B-2`8w=s)kqW-ZE9uVQ3s#Q#hCLX{0(*H6;Rw?;fHtTvNQiqS#vc%&~5cS{| zrX~NkM1LIw#Gdml5d@VzR7x(B%k5_u@#3%YAU(1yj4?)~JVWrHm-AulUVqiP51x|( z;+bZj@tfB&w{0q!yZcjwd^CPOFGS9jIp0|grQnwrF0SAxBZ+pVIw&kMX4OMFd_@u( zI2V`_&c2t47YHbSCf>Rg@QFHyFRr6d$E z=mEPF(06++ubM4#=k}*QQyyn`gZ|1Gs8~n=`J#yp6c2$=#y%5O^|*n~=T;>Zmg$O! z%Tbo|TinIFks8E}$BBPAb?6;|b^)CbH#059zdb8boq(i8>x4=0^!{JgG?H#A)>fTf z!{Cl-&gFkl}i29ae1MRoY~>{RwCVA8jb8~NI@I7--o(HJ&jH*R+^_y z-)B@DE(P;Xp@Jm0PFeN^NwmnAVY%ZarzUYz;h zvQ<6$H?xjoeqaez=n#7+*jx?QeD$>uD7LvjajwO>Z(in36$9}1F8AJ{j`JboI^mzs z|IMQx5MO6&ohTS)Or5#s%OjU1Z`r-T-W-W%MjpF_gRn%R9f>Ak0pa|Hu#`thmh0@_?Zy8_u1`TNUBtEB5TJDcySqGLX}PzRM`dA@0x*_dopv(QlBO;?oVU-5s1Bj5Jj z*I&XPF-s{Q&8<H{L`wCX&baIpk0O8vz|~h2!JFglee&7G3FAiGp~RErMyWdX?z` zDWE*h4Fd{5tl=Ce+p$@GYNNYwDp$|Q(|BW{rRc*KO`@l)0uwc4WXa>X@-yoD2flqz zE-b%<}M0M79*^9%4}0EzKx z#h1#a#yE+BBd4R_98>%WuTk-GTqbHyW zR+?AIj+r;ot|ls_st~dIrf$q-+qq1@~W?B7@69Jpx?|b9DNPYZGC-S4CvQ2eiig@NpIj9%1OgbNUClV zoR(}VgpC1{#`^{@UzXOVXsyf$#N|^ya9c;ue9Eh@pb)LJ72UyE)V*jX;^Jjjnxc_F z^d%8Pd*inwdo~?tEPsjW$l+(P!=E#tCB?k&@A@V}!Ei8G7yrjImxCMKcIWY*d&^pj zG&!x(JC`SZIWk@Y+?lHhYr>OydK9z$DG5~^fG1)<5X-ccr~A)=KKg*WJSkbl4EpzZ zQi^!I`@AhNGqX+Y(_?=7so}E31(uge?7x~-4ChiW>5A;SRZvve{Yodj_UrrF^afx4VGE=Zl?c3h%f1& zD-y&suO50@|JZ3eBQ_DS;xKhJil<$xfLoNj_Xt3e>z0%<;@>!k?mcvjI)Z*fTWjg; zOCs_r)fg~Uk{Lm@+Skw}r6o0c>J)+;kpmOC)P3wJ0V~cRsEWX9ua}OLp}Pbaq6Dt@ zcW~c1`gCS)v6`@z>FwS`83|qM^+La|lKS8=OHrcm{Re+66`bI183ebK&T0rxJKEtV z?0*fFhmn92@|y0*qAT}i3*%m0NBA>aPmi$u(Vu%BYjSgr;XApXt+Hx_?6c$|jTXA) zCcg*?PWC%_+Ax&7)`_^uitB*AE!ALADEHn8JHtGy@Hp~ASbQN;lq{5Rrzy5O7#!9L zxtgBJm!|<1)5D(-VknqgZ=MUJsh8;fXjD>(o*>ATo zqV$$XQp5Qz8s>|fgQY024t*=6(a%gGSzoZpNCVc5_j`xJUfxV#J!k2s%4spmV!C`#V<_D!&?s8QP%m5%tdhsQ ze;JE?`iNDyP@~5%Uh(-+gznDCS(+IuD_c%rykVtVdTt0<0~DaRCo8fBic9>IKPshL zleN?ygYEKhYkcTeNguCIdzNK+UhK*{Ete5lk&Dkf3$_cQ6;?KMYC7PhGvr2o(HBLbE?1Wq6$ivPdJy#P;LmJY_l z>b0-tM?b}prifZnWk38i4M`rD&Wl4*ra#TyI6$iEo4jlJUdV&9q1M{i_F-1| ztD5iaK?Qq8t2)CCF9g3(WV>YjcAJE>^FVuDow<^3B7)MRnDaC+USFm!oP&p1WMhIW z@imi%>_yg6vo`mHHu#w)pE_>5{-X}{s6UL++9qf>#1PKNY?_|h+n;zu5_qBjIW;#G z(|cDS-uMgrqM?-6@LI-`rpnx3#yd_3vuVQljWo_)(ock_bF(?Piz$PAEdTwVoU51k zj)QGa19xTk@6_ub-{k>*1ulUS>_R}k;GgzWQ${U*CLw%>Bl}T;uhu=9Ufe4!swWf9 zjddQDo0Cg{e%?$DMh)^%*Eft4YcD2~vfGl08_g~7kWRMAY(+3(JORhaMnd=JRjFSa zN~DjM)}>*0L;brwlAmPZD$XN!hXWw# zSxXHuA8`@~m$smWnX{inOuRn*-hGpxv?AG_x6@mD0OY7CiKQ?L|*c~z2kd5QNVLaU*Mv<{ek{#eG=%i7{(m_e8N`qms*au*M+_ckyo2$h+&}-^3u;*BLL50%k*EFB@L<0r4b!6tXj4yH5 zNMizc{B(mYvPU16G6(pK$~a_L+D>Txai!A!J_SD-`dLbj1%KCJ$J)~Y`Ul=2p942eCIccUpFU5iR1(lz~~#qn8-HN4KfbnKTl9+>kC)>xq+ zA8St~IPj}cNORL_;d%a)-Du&@#H7a7splAXwG(%+L|Y<{aTH`vA}>oDXK1M?&Y|35 z-{8~ecY}aR%sroS#+c7kPA1p=t0R-nJC&!BK$byUe9oB&rEnM1<2z;#ZP>ZUpx&SZ z8b0s;VkZ)J?1Zk03G~mI!vF3V>;y`()*&=;f)7!DX&Y|k@?N9&T@AqJSg$NrN-!5?8~Esu~=vRUG4NVkFpnY zb}mujC(M0=+kn+Po3z`jD~1Y7+Yart_x$x)?639KZdcv*w4`a#@# zwf;T&H3134Q$PDY8ZY5n)9WA)19B(#J>7FIOltGupv*SfoaUXD9AnYQ?V67Sp9_hh zZttED{-_O@WJ$D28PT)ob%H%jp-uUJNPF*~sMh6OSP_(MB;!T8Af180|QEwoEg&0C^_ShGvAuM_xasZU)?%~dv6tgShZKx zp7p-nPj^54bT7C+6HpDAnX+n^knL+l3i_)o`R;q$yTwu$bm^-f_)IPqotb7U8!z_G zEB8cyQHrkDxZCMwa@^{P5DzS(t8o8ftl55VJxlUh-9hK-I9|Bq@KjSP?Nj~M^79o$ z*4r4r7Xar`zzU%$Ih^ODZNHOZ(mVgM@>wnduqZhDY${8}!^jW^yaBKV`%DYrPro ztVBznxaZR0<3~cEt$%nJ*8fMd9;TV`Z}Oshe=8Jehj?hFVYxcGHsM3YGxp3~($~p3 zI1+Q%OU!8)!`v8A)sin<22+8Q6-A$7ANvGCTUJ z&k7dqeGSDYsWr$XqXL>shfh9V@pJugiPsK|ZQTkRz)EvOivLjA_J1Be7C%B~@;JFs z^9Hm?K-}KM_2S9#_=Rw*ENHNCpzi`x!4C!h)+)>9c7)x*R=LE4XBiedI`1{yvi6Ge zUvt>6f6=^oaQZ#)8lL!{V)Ps(XaHa$M?No!0b3`8`sfe$^=ztDM3!$fRn(NX96C0T zS=a4574D>Z-?4AJ_RGQ<7WxO-kdEE2DT{Wiau?IeEv}y{q#}?f%j%hZI->bn@SCXC zC*xWZ5Y(r^$uy>Sof!;*lPR4G)P6Quk}M;=mUKB=)`0#7M8+J@w3U!Ds_xg?da#Yj zyJSsi6=>h{%ENZUrZuU;orM4BRsHbN*cMx%K0JObDZq!HcGe{xS-A}P`ZlIUYc&*F zJrEJ9^mPLUhJP=;!qrA`m(Umo{yOE~1ja0nl?j)a)xF!?bnA*WI>-V#_Oh1U=Qxyq zj!(N1a0a+B_A|dG&>)cu0*Z_q8pxlbGSeO<=ICG#n07i0(`JN1-m|K{eCcP)6S~fo zM|lk;4ufmbj-zh6blnxJ!?eBalAxsOV7kJgX|{;!*8{=wJDmM_Nz`q;u6`oce#Rwl z^U>jay6C3OFJDYmRi~fNhX-p$RyVna8;E0Jeu_Sc=*(wY%N{Mvx^G=p3{eV9293ZL zT)0`ajOV(jPTQo|v06IA8KOI5!1Ix)^y1Aj__3Gt+?w$-vha@KuYDq_>V;odIS|XK zItX!_^n2eI#t){E|39P%m+!j=9r*ciCg1$+J08%PW?X!{5{v&arLRxQA-(G91^N(t z;2ZjjYUhLz;IXkh;Ph~Lk93M9Z^S;IQ6gUA(@W~AlBSBUk~*cTi#Es9cR;S{MllbT zfuy{Qxw;?cbJDRLNy=W#@RUrW>wm?qGASX0&0uiZdi1o5q&9=D1sb|EZFo^7?GMq&QwJW=1H}H>_|UWj zc<$?Pz7|I7Gw11)S&{7$+S1xT9V(+~=Q$DK-*L>Qr}yo{ghylOve-ID6%Jkh#3aBn z2zB^_Eg39FFNIo}7SfEgA^(RESH1UoGN174VFOXzc z?xuGYoTUhH;y}Z%arSs)IC&w-Qu34p%P)+5bJpT5B}8$y9i51sH?_y@7_Vf)8fsH- ziAkgxSFLf0Z2-I)!)*L-quAag-p5H<-s2U*}x`;4-BEwkg%ik_mfn>k7O%J!BWf&!g+ zE@#5A)Wehv618#KiY1a)b6+`aOH;4bI)S#tP8z%fJlBLZJ}jt3*p7cO*ZbV4(GsD_ zpE%RoXJA+pLo*{IKR^;hMI>rJoCzoyTGmoTi@4?FO0#l`36e|t~V;q5zuJnv+)OG}d=Z>6QDf3BdQ@S$KmQVP0J^==pN32&-$!{{rIH?KO(*dSLI--!VVyO{-s@RONDKR!KSM0?O0&-aKm-$SNM;jo(8+HbBec6-k>M{ka-UODS1z(`tT; zN9M}3a(l_k|t!F|TLz0ZHMDt8xKTYr~~xR{#j00XsWi zKS6HO`~Lb-$1Ny`rUw>G>Cpw1no)jLxs$>^*Vp)q?;z{?hN&QG{}wQlm?rzrTuOZ@ z&xR5Mc}Kd25^VuMC?rDJWT1j-ALO;Pe-Ti<`yw_FuT{?Eh^%+{eUrsAwW1i;BZ`1Gaw-2o`;Pq z)Y!P^fw}Z#E@~TjFGuiU-T)UnyJ~t=s#t&jN@*rF*86pkJT28{DVvP?cANUmyC@l( zD&cHdjR52=uKtqhm@HVp;Q)Ru<~o4VZzbo?FaOI|2Qn=~PP5@?DA8MJ&fu>*qA&KA zj;F(_%6Y5jGI*^ZYq!^8Y+x&pEyuK#t+q15m`WJQmaptu!M=Y-t_5-y51nih7x0Px|+FYTSeZubY7%!VuzX?P|6*J#P)|J`#)g*zMln4rlT6#_b za#`;{dZ*}nGCfep=$js;%GUv_AM-wzgIU zC|!SMj{a79Gu5{Ob-MPH%wnI#9A9wx^F}Txe3IPpZz{>QTj+Jf30meB9FKM0_C{Lu72KsP4{yEmnCx4F}LTBFU z_5~Uni|N!r@$aE5m8a9SV6bBT->y8L^|MERbYvM`Jw1}^$m2+jP!txr4CoG&i7#j5 zh2Z()w*xG8qk6-lvR*B5F_Lp^?*ziL>(exP8)yKT(y9s%OznNFS^8`-$(r=&*Jw$j zbUJXe4F4&Ur?Fpfi(O;Lvuo=p2lpO>f}{Z=$*)GAEr?=6I`sN-f6d^lsTvDjlz(3yZ6RSp$1?8h z?&0y4mR*VD4hA|-Q<0|;7!8Q{Kh*$ZuUZ~*XM#3i>Ay~YHpk~XBq`bbGeA>6_g-rv zU_IGxc|~{tV)2)hFUn}++3Ii}EE7;s=Lv2A*?oIBQF$rQ!l!@M?DCev*=av7LuO?fo`cQJdO#53QK$d~~vKRzzC+o4BxjY;$a zCkn$AA3|q`$t|Jozkl(L09F27BBj}!eLarmVHe0z?!xHl^jGgS-)=_Z?~L5NsWZE^ z3mF$!QdZjVd{`gM0YY65J#fR9*}7~i98cBe&{p$S-jl$;$p$SCOMQ-BJgI5=0VN+m z;!ZV-0@8DR*8^yPCksPL}6tg`^33=WnAg=rN?O{YuAa9muqqHKlDj*yEyP&ISAK0sN&@4n0 zq{{x_IFW+43@IQpM}8t&3FoxO9<$ox8t!>+crm0yHRbfhg694AWo1sf*^`^dXOy)3 zs<--lodiY-o+rKf2h&8Q)+qWKRj-J;{HAGxdFX-xL8e$|Utczbpe#Ck#|wLQ#AW|k zUi^Bar26-bC?yu+UD}{j6`UgQ(fO3;0^;IfgM)*!SER;%${o96zOm`$2RyY31OfVZ zt;5eu=W{9oYw(aTgUYC~9~oxnEGrTH#w z!IeJE7$w~?+&=v~cm+p%QjjI=ZB>8yMpCGuQc?)SJSD(Y#4^6XU1(Mvwe`4Gf=Les zVc#n>;>9rMgPePP8R2HKAF3Sp)ereBAjE6-uZKH?hwbn?t%jxvD&RBdo4Cpp;VGG5 z&+Gj@$CfG+Y?TQp<(^l=J2iRwf*)(z;trIC2Z26EJa(3E$);#%CjL2-?%HLUgs6O0 z7n+jOL**rR=7t(spk>DQ8ZJqMM)kvVb>n%Rnu3oNeb*--yryTs1X}-6UiuTl3>ug> z+w8Q7+@|!PdoGM z*bi8pFaWGh;L##F*$4y=;no}Ur+3lU{;!v$w2203T^Z5uqX*Q-99hm?GPLl79FF2v z5sw!y3cDMGjt(s7+j$d{mshUdIXvOH&kl}}cuO&^K9cw`WJTe7x{c7#74PtXaXt0D zqQ95I{N4KqkCt5`QPn5f-ZV8T!sg;W^~^eH3e=s6IE*JrdNBP?=r+dllz>m7ximLi z_;M;GrX)Rco_~3a@6(P#%xvuu3DOg7{pqoOm#WY(!BOL6o%qkS|SaLbLQgalI+ zr_?8iRSLS_?g}U_H$b*P@o=gzR(?YbIrMSCqZGk-w%7L+@hhs8w(xAMrDM3@xnW-0 zI;PHcr3%L_?9R{pm3S2p!~Y{nmgNuvI7?P@=__OLn2ma17Gk~5B@J1%_1!;4cW*uH zC9jP5PW7iEP`Uwc|e zQ*<2Ag8@AYuNArkCz#c6JH04$)u3&?XW+Eo$1(laLGY%ha_hR<3x=~mAZLlZTdwfy zJLiLd7fvN~=t-^65PUK(?u%%b5kudgyZdzAGBak6R#mfrD|6cpf63ZrE?i>p#&`FS zhqgtXhSztuM#`IMhY_BEOExYbd56FN_01H-p)m8X`TH^PQtyvd@Ki1zrx@?p!DX+T zN9BEj38Mxx2eQVoDuB9zR?3-Go0fQRc{sB|<+cicod{-OsN~3#q6fOH);Wo)o~mRL zb$l$GyVckrk9a-c=W8|X-&-)|n0mVDXg^i+EI9-Z@Ai%dh5;VC^d&zbh`YP?OBsi- z+FV28yJRy5h{GG=QD1@b9rAul4LKteM+9!K%n9kP-k%yuQqD`fd>YvYiatj zJ4Rn$lNz9j*=l^H*wS>+lPdnMRWFfnDR-lM{tqrCf^aEMZf&*D261V@pM;5O89dbF zSJ)#TxVOXjCK^Hlh@{uhpT|mpHD2|!QzkHH%!@`d^w7aXvzw08J?Ppagf$B|%S)!g z@l~|f46p}En;%%?2O^u1IZ`T_;0L5L1EF;#Qdcv5uOS>=mK5drt!)yM%gd?ftmX z8*KfY-SBDu@9UG@bMV1;IPAwV-uE^g$ces4Ds1Hn!q{_CHxCq3pg=@haHuv9E~sj> za2<6SO?`O1*645&9r%U?o0 z**C`f&+j_BAHjXC&Mv^@5jQY-#8cT|VU$;yO%l|3_kqvg)-W)BU|i`-5_MaY&oPDK z?BScsVArU6uA}t8+zkgVzM8mefYm>InE8_RlT+%D9Ia&^vev^bW%03G26jHC!G0b9Q|LQxH!%4vx@V z&47;p<-pxgPC8TPC6jO`;15|P7dv5amZR|O3cpEJNVEh}JUv(QotCO2FGk_z zQ{WST#R07W7WD?vzBA)Zjr-*GmHbsSGfvm4ukRKA%0L~h1ErRqae09)j`lGJq1<{W z4+Dwj_vvukqm19L3rHW$0c>w^OX4y-5xoao!uxgIjrB8h!%8~x+9!uKP>&UEvtIXXk*~h z2>jhh`a>vv$xptk*V-8fUGF8qzTCE`1F$ix#GH(vV9YWa4p-jgjqdcYkVZ>1^gRQ% zgMe$9+!D173^*(8M5Gt{y@)+uzHMa0t-3L4UR&a8`7bOY+oCb~g2BIp&+g~Q#%F2`zibtI zapA^i^NBz>s#kUnE7hDJbyC-pW@a#c2ZKB(N`l&*&;V2cUP}oB$dBF zMD^wZ(82f*|72DII{EFdIUEGJUN-HMgV~H+^sN_vx6e!RnqHJ$$-~;2`^EMy%&Q%X zfKun4ZNy^)U06o)Ltnz!76uiMawJG++;xd|7{7Ysat=z_{h!9{Q|M_}MG)rRHnPh2ghD|VzugB2I}r>*?NFz={! zA|=N9hjdAc11)anByZBzv9Ua8GgMG3rm)lGItt(naC7NrbxlW0QwvdIb=S@7?`A2m zD!uH_RCu^kxMDpMCEioqceEwEInH0@wvzgjCBs|KxdwmBMee8=*5zDkQ56xSCBc6{ ztHSPdMStq5dc$XbXZPfEO)}4r#FMQtUs&1%+W;_7Rh+{m2C2X2VK#y2> zx*SLj(4atlfreSXgV}iN+Q|oD@?q4EP7oDy4qy!bY12FB+{Snpe&6Q-u0O^x(-3NN zzpzvC*xUd8E(n)}d;>^51>W?2l7W4HZ*Kk2vzs-qLY-oW2IEtDUuJQD;()8lv0hho zGl#cbd@ZTh8jqwV-YQ&2_&^WC=FuH*w<@W5uMtr#IuLL2wpDMSz69?*g{&XtY}U>7 zkWMqNvO1!Jd|S!}Sd%B8!d)X-R#f#0uu+GU*vF&TKP!XUb*8cfhuOnuIA0n%^dJK7 zq9cSg&sJcKc|$&{vbxDpPy|mp*gE4F87bQIizs_N-ok60m#+M?OHeRYnCrN>BSC7t zree5edFRY%Uo~EqfwkX=cw3NhwWhgu6WC4Ib%lj*z1YbFLD{rVXWm41zGLyF0_2f0 zi{`Gw>q;+MzUd|->&RzH>|EqHr71me?rY1;eokq-;uoQ#r64+*L&shZzXtFGnv18& zT0#Grd3&h4+fV-_E-?;c6T=L(1 z7Ly8C!}^KeBQtRhz`ranOEKoE%2xz$KM?;gKCOxeDWq&dG@~@%Fdi+l3PV1Ez7;U0 zNmt1G{XuJsPEK78?WGTa`SwoHg)RuRyH_^OhTv&z?nTCAJ$NI}a`%Dx@vlYa9gQz8 zA^j6#F5uSvoJ}a7cT9AAsR*2_YYY?Iu~4zHoVTI1u>t3i zM6Mox#dDw%)uI&P@pPM>bh_m4Lt)pVtg{sE6e@Zo+84eYrWnWQn^?H`ItGL{Mi08y z`UD4-$LIs9g|42g-CYr$w(77uWmJpf#PttAV4mF+2QbT5ox4jB_!Q$U2Uz6Eecso5 zu0Mb`Dy^%wD!f}?%C2~jJKcok%nJ0>NmqxJO+6W8y$}@5`Fiu2&Ckz5IQPl11z{A} z%&f#z$ok9ioS^$qS0r%rV$|{sIOW$N3g>yHH}cgS0KYsbd+YirhV0mN)mYOxnXZYR zt~@VvX}LsEaHZnZ{@Ky;3D*{TsZXOvWOV!^n$Mw;DH!7H1JndU)Jbl&H*vm+7I1E1 zVliBe-gPV_vknY}BdXEj=EmPjSt-ayQ6i&eh&{^U4HgOZ05R2h zjoF@X=+2sNazGzf6`3Kad}cG5BVvGaGyCLv9DntcDxmM;)8A<15=0$;=}z0J;WJVG zx_>?Jf@8l8DQo10a7+qQ=zobPe;M!pdYoS@sGyEA&g@;{Awt2qRMDg4mvh{9F3F!; zIYLX15^Zuxw!-<>@;)y4tn5}S?t-~`3O}|8p-C{%PPHve}(ABl-UIG)Gb1oV_wE-KtlqBdEEE=-68jBc5`zb7dyG zUg4!ytM|-L;MfTMPY>V-YQ@J*{NKFnKm3ywm~X-nks>~RM+m)5f6xF{oywpqHW@T^ zts}!#5P9$DHJXA=&?CQG{LSTd1QNKIU1VEpG}EFUG%(!mxEPf2IMdA|R4 z6jN`CA;0iK=^)juzax1jnhLNwhdUhNMCSvM8^9|c^T=mL5$8s`($0vh>qkNqRZC)v zHAw#Opb#HHCA7;!4xu4iM$qRz#^1opegm%MX6(d00=ciarD$iKn=&Y7#!%(oo(sWe zrvomf`rE%hLsm7L`=KJFI@4jo5w)YZ>+!_9i)F4tfUX{T;D>A(ZOgTFjwzZI??CMX!Mg))ou$0tFEK}tHN~o)Arc~na*$eM<8JLohYAL7Hcmv_ zPVyg|lJT_hQvZDk+jogbXP^Vkvj+b$x%wZ&`c5Oy&?iVlJn>XM6tA+H)3nbjiTlGD0wbVlXSyYOuH`9W?Q{qB%iymx9Pkjs@OuBpk35VQ7Ylcsqd zzZ9h2b79`|9cqlt^4(P5SkM3J<*#HvB>S8Z%imIEOEtTSgb9e+>`B~{+Zh(O%)N`k zX`#B8kH&B%)@@Zop2DBHnx1&dzN7fZo~jA*W4@WinhdS>sC-iqAR$u$Mt;+_w>h- zN#`}@3r3IWoM0~R4F-3^D;ys4whsZQi`DwaW3({&D8snW(H@OO(2H;DIGuG=9*V_* z9(A}v91c-HsM;f1qhdf-kJs0>7B<@ z=Wc4b3U>D`jrjvFjoc%jpn7*b$}Km4k#8f9YXWaId0*=L2e)VmxV7x+ zCj37(^&ex-F95F>&=-i-SpRH~WeG^~K1F*JGF_81cSdom4mBM#1uUn-?lIG^W}&4+ z+p0cc(Z}lK_A5rA4qdAhg}1DxSC@aOQvzpbK@_$sr7bpD1FKWgkEZu^K{#WG@t-vA zrBdgpNbW?+;5FJAk3B$UI^ZsI#r?569GaUePa8j3#d%*Fq&+dJ$=A&7d$F!8^0&{+ zZ`=i9AVfxM{qp$|KL7%xE(Ceq=#Rh~OMtAu{8pi%qyAQ8W-=;hB%G=rNy#`U#3r^q zE-mR`oC%8_a4nk{t23bErS$=9$yxnn)BcaaMUXI$NZ;CtTFH0cKV85VTnm4hn%s4( zPJI0H&Or@{f5j8GofbLOSWt|+*hlmRz1cAO=;&A@y`876?;r4edW(4Cab|J%NhJvZ z0fMfxY8x6(mKKxFOoqn1JRhDdz9Y_Oz+Gb@S6P+-pjgM;FFD>RB?ME-8Gx?q_t`J2 z?G1i|IrLEXbv*SQxy||6#hGQR3nLXM2mAIk3Gfgr1%3aIqnW+iNB~?PC(@*vqAw(}kTCz`sBTW0>y2Y|U^# zk<%y7cauqHnCXp8$QC!IBv*^Wx#m_!(O5UC{zxPo6eNSTD-p9HeA zhny$!UkDQ51TOdk%Z153po7E@Dr$pDM9}CK3UEAqju2;qQZ+41l)H0$+;NF|jdio0 z0i^$7v z5c;)yCrGAL(c~*~m19BWr?UF&Z~Dkr{W&4{)6nJ%-X-xIz4cD6o8DL^k#M?LYZ840i5A)ek-!6cLQlDbQ%FD9;tvEOd_=MhI-e4(t0`*O0f=T*>U$^_)S>gR?oL~WI=TUg3%9g);+IE^3F1WuQcaJf3;elpa0 zgP9zxvibb#u!fQ7w10tt%RRtWs%3Q6ulyf@0rls3p02YCGG7=GcxgxX6W+@w8d7|shP zS=P}_%r3e)6tcv95$aeS)b`cV^0yx%rII#!V8p{zZMRZ=txhxL?Slq?;_!F!dxXO9 zUit$p$nwcjD$SQ7ja0H<8b6&+2aukObs7BO6f?QNY(dPE{5RMbML2XtE2mh6f#qV} zUq3ns^o(Fte#KKa&u3K{1gi=-p7IkSjhw7)yj|-as%D+|E0wxz1#}=r)0>(p<3M>j z&XE3e25<^`^kfi_0koRr?8O>1!H-z{+aX%9dWM?>Z;H8=~FB*p5g{=-F-igu6 zF;zL@3I*giipi~$c-0Ms3$Wg#eJ*IuT1UX|+o30XT4kk{8^%V(db2=Xk0cA~)B!AL zLjOZGIzj{q3Z1F-S6f@Ar@&B<4(?QWj;GegV#IbG${K%!^LDC`#n;3t8IH0es<%Ya z!LtZ%HAf`?D+mx91bPabv7lf66!rP8L=$+N<&5Ol?gyn`OJz!CUy5VRIMOE&S}{&N z#4v>iI0VdiP(tMHD!_}JZiAM}=AP~Dgn#e=E` z>b4naMNaOt2#1`iVphzisG$%D=UrIb*Y>2gULJ&t{v{<@YYC`<$e;K$A0L zky?=!@c?_l{jUn~|K6_3NNrp9Z#1j%?h zEM)1|(ADJxx^$ptt!ydchvV&tp4>fW^f)acIk+)~4z7lvf2G9pO2^QHOV1Y;0Hqwo7YY4QeA-~^py_EG4^xCQu-+Fyw~QlLI**w+2u(s zKY_}P%DSn~FXwof31vYiK1Dhubo;-Eo1D`($-wY;r zk&Nll*uV2|Zjz#X(*CG4et!68aN*%D2iq+mbkGs&F*L6HbQ9Eb@erhjo1n4jnmAtk z6hC6PR6G}dm`%2QEl;>{p8hR=FtzRx}jNxcs+6+X@EpSM3+`nN+EN5-=OY>OF~4VF2r;vHyd_|0TQ!Im-h+BkoOl zJ8R)DkJs|lpV1ftJ^kjqmrpdP3`dGwgk~EiD$~42V$YWeaqqH` zwKveWS%piOs?AryOG8ArzB$~?0Ch&DRtVDHdb?k->FFv3QPPV<~H(sBoxdfVi z0h}@6U4R zXi?+*5HYQ$*5wDpK>LFg+$rDl(lp@9fD>sH=6Ie_AZt*IaefnCrTkH88+>bT?WyVr$%yS8VemU4!yW9lYw9D{f-#pE)tn( zWV-1A%!fcVeZ+Fdn&%);|JZ8gm+qyGY2=sk2K1OAv?h00H4Oc#Yy8Yn>(n+qq#Hfx z-D^Hz&&27xx(Gbrf0;f+)i^I$ab2&k8}0-j1DPm#O;3BV3if*N0b^tY2ud7qS& zcW|aYBM_5*9S586K)HoeWYashZJqWG=S1ALM``14dmoE=OEXn+bHAi%8~1aTDi?s% zMCt+$_Yo(7zXM`I5pnYSL5Zx_rrfs?h-I?IEgMMPlHVXGy4p(K30w=3q zph@(vJ+#yGEF1g?Bz22KL^jNFV-#p6Oji>{+gjyx;lzY=H6^a714sl=p?<%8*=-?a zj$6?@`ViXR*&{E$m1+hAGUbGB%VNzCeIJI>N?Uz&eJWgk^uas-4Fp8AGJ$Q=MJ2L= zU7z?9V`LA%9sK%<+?{afQf6|x3$Ndd6A=-)8Og0D)bQm?lYf<()z0!r!?Kr=U9z9~ ziqq^|68-EP<3p=_$A$yCP1spIUu9(_$==HwM6Gup`0Xoirh;+5htfSZg*N9?U95D^ z);}?Rp`F9|?=ed_AAe*tvC#rdNz8TkoPZNe2nyL`@r`{{3_``~xzUV^8-|`a69^Z3 zN3Y7uKkCUJYAC{@h2&6@MZzM!nE4bywg9&D_?5D27ZfmtX8Pbw2-gTVX+m$uhq&0- zuU4uC6%!|AyCO}ERO$3MI~-gc_#U#VNece%EYRD0ZF6STN~8UWkiZMet?6u%iU}VJ zmW4ooAgQ`Ypni>r?97k7Cf8XfT^i#PK20PMJ2)72IaTBM8Uwep>Te&8%WOq%&3?G2 z+zFsc>+TbmMvf%SVk=(cDj7 zMXsRdY`8K6cp@p+Y6DN3_5a8UYDu%KZc_o+z153)+ORuYY+2|$-!KXhFw|E&R|jlJrTssX!PP@DXutA?6hFYM z8vRCQsg8Qws~Xo{FAHa;vZE(IAR_$j1;o(FZN1iEI?mMJuCVW^d5`>(~t-K}Fx zzbeu6odzZWy?s5O((LAo+vkugd94SG@|mBB1E5=G<-adC&(51T$I2c5))|MuSUSWe z9l2Nnk3V*2$ULWOI8+s?b}$qO!LK!`0KT?49t*zV2o4!swnRR`vf(mxBpM>0vbZxb2tgF*|O zj;81%wjS;xw9Q=iYsp3|v;0CjDCSWgP=P5lSa38XPlA#s#$K6qDy-Xqal*NL)R~-n4-2Jq=04H(78>fDPv0%iahQ#k2 zSPPVSD;^KJUrf&^riMQmeDeetyAJEM~aFKAo9|8#$>ODab{t(2mSMvZLDj4JKF-uCz->l$#G>ON-> zbi-%(Oh9(O|LVBS28(sW{Vzv+;M4F(IsAR2s^UjrZM!~Tq5EGQs=w0x{}zn&U}>dfzYuxmJ>N;h2&1IJ0_zapouhXXnG$OqK7_6h zD3;$D*B9M5;p>gbu8pFBemjumQGmN0JvN00nFmHyMEn_llwP1^(b%j10lZk)HpJRL zLM*#CS5|%k(OmmxS-&qgG&|CW881CnU})Tb%X82n6lU#~?!HT+Te~9um>0?Nx}VZw zQU)T!r}XY>V%VJqE$0)A1|E4`8E7!qZpU>5IGtVEjhmP-mUGr*fbrJj{3`fokxPv` zu7M{j(#hWABmu=qZ?8-1U1uo@K2jcs;)L*$mk1fqJ<6UqKu-H;Zt$zx?2b4nQA2FJ z+c3_sIS(d~WEmqj_+i$v9oUZ?xk0q(NcP_j|8JLIRt2)fuxSn-@I9c`F14>0qm2>c zkk@jw`%zEx9ZF)))61*(p$yOFD&0Yum^y=Aw`12BZivY z$0MPHfn{>K-l)!G*G9^-lbU&uwSSIy_mNNZHVZ;CEIs?xoUYGi3T84<4%pDSf0hwf z6yWK@iM~F$!QNQ!R|uYjf+zEe5$bmWY;(ajqbbD5_`u(bD{L} zP~1E)p6ETTQ~SHkp&nc#efTqe{bWE(;B4;6!;QYach=*tn}2Uk;>MCLHYHo!0}ef0 ze&2t@L#x|Q9mqD8A8l1PR@S8jOh^RCLU(E0dbW!!Obo9A(UQb-_q>T0p=!Ce6j!H3 z4e-8oHJ$hgy{D!8OB$qbUgW1@XmzmrR#}ar;;!SVqvjSR?Sc|)B_1>RYOPUMwBh7x z$AzHYyV`_WoW;nw^R@rd=hAwA{{-cYAH?gNkN1c?A$A|NW!zK^J@yV1*YC%Sd$LsK zy0yDAw{I(}?w)lsc=EB~1u)`d{|Gr-(zz$*RVQwG~BU9CIt~+_<^Xms=00(JQ42zs5)@RSDadZl+ zzz{gly3lzhySMjf5&!frVRbe%U-DWvmdX@6V5t{C z{Ptbd4tib~Kfc~C<-1L%^OmOy58!SC-?EiMeVpN9PPHrM&IW~TOjk;&7RiV?BUz@S z3t(q|B%HyxTUp3We6Uxw|6SAStG?t;2f>hvo8?EM^qHurY}xqsyF)&n)|^e=DnX-W zq^n;0CFSd!Ql4Y3z}S7gG_+l3trM5_2nD2ZXtPFvg$(~Jocq$?{F!8PIt0U0k#YQX z?+F90wEgjGE}EK!(lG0vu~`qHz3R2|6TDkqIQN*1u4h~s>!-SUZ&$aPWj0sSYXPef zwyU5&XDGW2B+}^$$ufsGgKm@>*Be9`h8LXLs`)aLupvvhst_j zyA=&D3=7-nKd}3-_PYY~d#yb_yDMG3k9e`xfBTkwrI}W-g}b1_o}wodb|Biw;5e}o zte=23x6%yR#5P~guR>rnP$PRE*_?o;FRp5@%ub=c)u`p4b{z8YTlplKvDZarbG%o~ zhRhfCyHH$RSKxgPhNq+CGe>(Hg1{brTtHuQjI{PBdo*R35(CTA{ykzMG#K*1v1)04 zb4u=vjUMEoiTy|^<&k##c&1Dj$TY6p-hL|$wGjXiPkmw)__*1Bsg3`7AYB1KJR9v- zF|sNIN(m!FPxqxr?0M>L$Jou(P@V=ZS-U`3Q)}ML1ZNuMg5ERx^0#v?8;OPZHIAp! z#F7~Rq4yTUSqnSc1-f_Ze)lcI&2nv79dc?78#@SVDTccrtH#HM&Vb2 zf_`!qQwrs7bDiApQgIoHffIR zo5pst_4@-+*=uB4OSf|K<`?^1VPEngALDY8rjjAeXB!dCx^Y*K5 zta=fk8n=2~a!L2KH5K>|xox9Hsw5_dm8JdB7CXeDN&S;TW5kJjVH*@-%G%FQ=52m_ zpDPlwk#6CK)v#YL{WvDQtm|%K*Sjt=#ep$Nn6K1%C|=wlU3T=b&eo-O2il3Va{H>YPViaviansu~ksah+GKZt(gM7MTETJpZ5IDHWVeCBx`-@v_ofUGj0s#zO_ zTBCMGlaAIK)z)jaM%8Ek{ATKK3GfdCat9nnSghoF;13cS>pCY$?8Qr$+p3UAj-S%{=?TJP|<)>zwiljC064oDD8%RkJ2ps$}(78vA{Z3s!Z0J`LXO$+3NZRtHLRJMhq=Zz8m@d6s- z1-EeQ53f=!J|ucp=`9nCx)RrnaB|+Y~9QaT}6JGJi?l(sct*n>?!{6s+Y@G zPRu4o(N8@msV^KY?UnZfGFoq+ECEU6cLTUv$;9tAqd57hN#`p@-+rCVHpj>=4x`4e z5~VA=1cF%k$*xqVnjq>v%&)vyxXKZKhjhjyl3e9n$V|1!M(AP2UTGDA-nm%908gCJ z=esjI|ICGYll5`m&a>myhIk*z-jLPsl73l@cCO(47OwOzgAWjZn8Uw_BO_Yl`1J&T zuS+A!8BPlg%Ga%D-g@yGOLVc$S?}qu_10wp5bm|Fe0sZ2Hj|41>jsfXXZw+~SL&^l z*)pbUL?w|DEPq+`_@`abK!_DM@7^hn^W0ic!(-rauI)qy(W3yD=5RSmtKE8Ls9~wB_25svx70_nry9n$?vBVoWV-ncOVbPSK@>8? zNJ6Mlnqph^*Xag@&L492&qE;tSOXKoXV``J)BJCxT&R!^x~YY!CRL%|pm*Qw%}1Ka z>}XFMm91!{mp?ZAS9ewR3GlOEzEe|t=XwiS!b`)vyQi!SZ`c;v`x$(9F4vQ>j7@id z-ML~SCcx{b<>z6AKalYF`r7ol5E4|ZJhE@`k)4uwLh~P9G)Z|+^8NNKi0#gNt$%+X zkm2}zTL};Dub0R|?%|0bP!^vc-L$vd9k{8fX4W1sr~WGJSNd_q#4Br>`ut?f<2E)_B_ez zJgjGg`nTVE3jag(aqpe`&e`^Uu0C*WLR~yOf4gOoBqywH>7>!i&EvI6} zsmY%d^CQ1bZH9hop|;l;d`#$jMEJ^}@pX>{8nh-wvmj{fi8end9KakP)5sJgC@AS+ zl6&Wpt7k$8ow_=hAOw94V0p&06QRB&xdtyr={!qaP*k!lCea>#l5iYnrPjJMQ&LYd ztX-ujSmgH9pf#Ky@my5)Llh&iWW4;Gts#GaRV{bZ&em<&>b0e85nq-RWkC&!v5_pF zy1p9^_f2&`QNotra_4289vZ4Z0w)e)`;Snb7y) z{O30QbAHu-h4A<#%4CyOW1=C`)_nl9+{3<+|4}e0_4h5#>xQkUzJJb^M?O(v2fMRk z{}8Q{|A=LJ9GAEeAzPzXYgt6Lqf z)eojr+#ch)_4y4}KR702nZImd4)S#_m2SESVqRzbs<-GsSn+$wB_(2InI>o&G%cRdXhSRbNO!Zty20mrz8~M7c!W-+lSZ=6UtMPRb)v- zH}JvT-B%&YKg8q8>%Ko4<@)9`qzGx$2nf)i#)S1PT#xHbP2-J-q0>w)Z=oYJk_m5f z$O^;+|36voUFWKhd?kR#QO6hEp_sSdhXFpTWF2(NsJ`m&hN=Ka@geXdG^js6 z2>9losehxCW%urGN+$&xL+@a55t<-JSGZz&a<>**@7&d6kg}JxZo$o`2GP;jEO z`Zrn?E)xGwz#s@&aM1ZF;DG*n?i7S2^tW9Co2J0kteGRzd9{Uj9!o8X$stSqccTGI z-UYbLy26J$3+sDrLxUR|Lzpq*KclIo`pgJqT@Ayy+uozxfmV^@6Jr*aBRhc)f|%2G zU1W0%rXE(k^0mx2T(9piS)>${A(2|Wv%;mc+n}M!^X|RKaMz{^Wm4NjqE7LgXsx}m z9aD0u9+q=AY##pU$^!Zwi7xB>j?Dp@uvs(hAC}^u?OjZ#7BbA@Fg6@whjTA7vC;a zoe#(@fXAWu=DATM{l$`7jUS3`!TE2rq-s|NoRoasU6D&_i^xh=N~%8+=l0(%PiRc` zKR%p%n*HHp+Gl0@p+BqaEb*%|Ie$$2)H)m7j5z~8i4 zeDI)xfZwH5x`=?mv|C2n`e1|~JHTL36Q#PC@?6;v{$4a83kF+n1hkAd*KA|YdcBBxMBqSyKHq2M^>l{5NhV^+S*438ZJ+X$4H^D0utsB_9pPbX6w*Kzs)D)rdl%Hl~T3xR$Qlb6G79jGhAUI zGLNiVwB{4K@(r?n-p(kiabtK(vaGGAKREz%AT_{Ra^`HNvKO+uZ5VFD_W}{(zPYt1 z*=R|xc?1C#9%(^_{ZmR;Ppm_)XyU3D^EFZiuzkq^^GP`VTNf!7rN72}KMIz9#YIR8 z&^I;iP1m~q7B*~n3;cI9)wdSm4z%s9#pjVd_jmFaRnza%oT_#ECzmAiRc9ed;lXxL z{dV#Z{$cW4d}bW6B@mEzWUjy0qiiy-2Rz0~W%Yzqji`CPvjJj7Qv&5xEHbVE%OAN? zGeAju&5@)9LJm`B-fDzUPdR_4cbUE3?T@-rc!xuwq?1$oP=Hi-|H4Z^s(I7u z8d%f~QO+#v>oO|qY;|9(3Z^uMTUDg$KA`WgHyF&ogi}Tj>LwFep%m|B{uqKQeMiuo z2JfT=f3~uL=DK|>60i|vFP!FQegX(H>KVh}59Irj9rClm*B~;+0sOqh+97Iyg=si- z4nHXY^B zwbu!b%R^%8u|;l9u>cVUKMDCwUy2h$0Dk)QBr_BDztZRP$@N`@>iIgYgL=8t+RRUD zRg>-w_g!}3$$T9+(>ToVQLOd7nAqfT$J*~=Lrafj8J9@ojzi@*e3M3yhd;M4yGH*5 z_F>36;dQy&E(tpYqnWzc>@~c0tF1SV)0ixizifU<$_bv6-YXajH8&hRo3d!kuPg;H zKZ-~1odQi9M6u>~_wAmDZsgn?i@;>8fm7X?e8h@^pd#IXt+jG}ZIQH!xbLKoUZ*B3n(#4CY3)775!VlhMfUyqBv76SI`XQ&RSsyX)C%I_2koQ z=yv(*V){s|iOCm>3XJ+`JLu$eZ+4%qdC0A!}aQ(frg#4g&3_06$13S5=6IeZ^H6WFfG+>d>r#s5xae+W21IRKq_~t{6P~ zNG{@-CH{iO-SF7#^00Fj7AdrEdsUlB6b1@#6Sp0mld@u>wM99?F;}mf2e0Da>qE{R zLIOx0V)@sJx}s_*>>mo23pMs9L1?&9#d$@jV$r1cduQ~aRtk69B1Y_g>WGA=WYw~*NvV% zR~1OWNw$uE*SNV)2&#Gwgq+9cz24LZq%pXx^3dL^rMUz^ti>?J>;9$e4|22hq1GK}@x7LSogqBRgLHbjUDh1FHvWnlV+eO4?ppne`haK; z4CVX{G{NXO5Dax7D(4buSd7@uI1l#KktxY5Is9jkhW2R?b%n>;hKya}Yq z4#R8U_|hn3t9Oq)jMU-}|U%|QH81x4_x1L^F>ud=B+7{tGC^?bkvwQm@1t=r^!g!Uti@3T15 z_897>M(p>*0R%)OASUD^^RgN^_Q+JW`I8TccuWrf($5#kMRbn zKWnbZrt~JOn*8}^j#>Q*A^tHlHDnt?@_1!=4a^Z^IOC2u2OKH3A_b{M8)_uh99TS% zb*PbpZod>erFL)sU_9P^+Tc^Eg(*F>_zrF5Ua?9C@g(2CN9P|Z05{Gd7PHS49zg;O zmL3lzE>tGQ zeKk-^M9$T&^SG6TbPdJLWKKu95QWe1|5N)k?*T9{?%AQ=!HfWs;$c9e?udDXVBv63 zf^~2A?VEjpyxB!@a)?LJz3JWtq%q+t;t}LU=|I6LnbJqd#UT;5yJa5@iR$Ju2Xws$ zb8W;aQ;<*cNYjA<$o(>2M(i;y_x&PoCgt8ajcv5?b}ul|z8tL2bK@O?=d+OYxzvw6 zMu>dxjg9W{h21{O^P1x{oew;l;JDKI+sv^t0O7XdiSs+;_0;!icBiY}f*Grbdro&_ z#$OI6N9Q_1sEdvsyb;cBOKYWvo;+&Q_u%K04rQXAtAqbt7Z1xOz*u{KApg++vrMFGLs`mE zBzH7E^JHU@{6xtsWFO62XF_-}SaSytjeq|cxMh9k= z(xvltc1=nJ7Ni~-w3Ds&zn@Dou-%H+UHBQYQ-Qe#Z#8UEM=(KT$3yf6*C8O?0lbyg zJ9o5m{o%@vb)OpDtX_{|@hVbMsW`5Jny+vEi20YG_YhkfK-qO(I`zMqfqz>eO&Q>; zG^VTFasTMV(^VuuaJM%pe1trP?agWU-mAWILN6`{Gfn`BO2xtaUu~UuMk$76wMuCB zlIYnl%8p?!Rr&P4+E27erb952V$9>?_)m>1@X|<{ti;Tmp!?VWO|C1SMElb1^W@&O zDTWYRF2C+nLD!*hu&(WO@t2^sn7JJpz0X|Jq;>ZOHMQ?P6w?Xmu6A{cV0a=bJCgdQLGu|S%&>=b#GrN1)m>HOxP+Me_3X8A-A>Zb7}dC1m9rO z+U=+)l2-Fdw$GIxL?6=>=Pw|+CR1Tw5JPt8%q02J%nK>c`$`;*heHk_`CFVSju7ig z3!BwdNoB`jUjxE}YYhA)6Kw+cD_9TCrXqs6PE*K-LvBv$RP1I=8^;>th=~5mTp2(} z%W-u4W=IP%LYl)M8M5?)C2w7WQJzeeKOc0LoU);>Ex6BIz>a0Y(UqkxMZE>5YMx|38n(urq4qdwhBrTB(G*W$MqF^CiTK<5*?{0Peu`2apPk_=$ z**KLZP!JN5ut#zAPVmfmQmZ!u5Hv53VF5x^bC?BrDO6PaB_c5(dOO1+lVB-&Z~V=r zhxOBencAzsLOIdpdY{eEWcBX+BIzOkXaI>?C;XQ-_Yxyf+g&OCEuVLo<^V%viPKQk zN9+DO9r6vZ=bJE7u?x1J`~w!ih;7Nm!mH?v-DNK>uK4Gm2+X*a>fGbP15q$4YskHGVqJJmeAfr*G2u;bpZ$K zop(deOzt&&v$@yovT0f&F8&}IN&zf8QQb0Gu zx4t_JFQN_@LDs~Qd8{j3Vtt7c-|EeIz6JLf&^`_Xd{dJykA}X#jcm9k=LWs#=fyf0 zggnX7lI23|NM>j^t62PtsCilHv$)OWe0a~fX#99pv>i$R+|{QOp0#@yNd#11-+h@MI z>7}O#fQ01{QelhJYm9`#LzCm1*6omXEr(AU%fB#vX%IVykd{gg*-r<2jyb5vGMQ2No_RjM zlDqLTEWvz-_}U37<|6t128@i$6=39 ztg83oY2004Mz;oQ2A>v8LEp6ka1c1cmc)geNWYzXf{% z5eb8d_FQZBb{ZOXB2i!qr>G^*7-rc7TX*<|fyM6Fia52yYd-W58Bq;z#W3p%6p zI5bOKCA1gsE&?}TPLW}|Ks=wX51w-ao{c-i=6fU@LtoVYTdor=E}Sapj=WeQ) zpm9xk{-@~t;LQY2n3!!4f-VI->_BNN6IJ{zLDV|~27t~NR#Ze~AN?CZKYb`EZ+1gsMmfa_%N9DA; zJ|5c7V|xaX|AfD*5qoDb?dsbib+J#0yRJR@Rb)0UV5!9$&0DvkV4TI^hN+P8c8!mj z9*t4Z?H}4;E~_V$@*!CXX{prxx?o@dN=hhgznfn>?)8GcCbdUOW+CczdV250%%>MU z8u#ijq66Qg=NFCPB?z=BI+P-9UhDn%;$I<%Nk`S7trEoyHG36J5@w$r)l*a^{d#4^ z#5qM|b#D|mt~h;h71-YOR^2I&x}WDtGf$1JzR^O~ia64J((_wPDca0>E$AS`p5|mj`cEn&B}Id3G9F#T9B7_aeN-PuLf+c@iDJl>U%~!9P#k- zou9?m2hu&DG}I=rRIk?y+sU`Rd?FSQbhaA(JpOy-$?zUf(PQtXSaCgJ1;~OPhm|+s zS8j4;MQd=sW&Ij7+`v%QT6BpQFJJQ$S_A;~6Aax|x2dm5LX7X`r}e)4T;ee47}+(? zH^&(2dm`oAR9(>2o^XbZIA_1&w>OIu)y42NRFiSFM8GbT^vwAptZ5FF+PySEhmo$bt{2vgOS=`E^MG~` zd55~I8k(ux8iWI1u%WEDZ&J4oc=!?$g_Nf{jZGS4I)zjfcGZ`fZ!SO}bWMgk!^7k0 z( zq0auijXB0-rdK-_qHM3aN2229aZPUqUv~BT0s5?3@s*eK|3IW{bgVo&pFCkgN@G8l|~*~kBQWRc&E@}$TMJ9N8HX%aOn^-OC)6b zeVpq4&o5B~&_QjXxzWzI^l&UzQt+X zKfgtMbQ-?olpXT2%tgeb1&|oI)9^E!ZwH=Us+dVvLPJ;mCPOLN4(%`bHb;yq<`2No z##_oNtw;YBk#YVOdCj%N$SkTU3J@(@4#4^i^80R!sf)B0|{ZM)kfA1~|zeG~Pm^_9PB->os=iJUT(RMApcWd*z#o?ES3p^~& z(OTFq?tL{yHEtI6-YOd46lqMRQjxk2vC`GsbGz_t?_%WMq99r(dn<~cjxTC1n%(%S z-5bs-G#9e%jlwHZB$rege?E)ZZ0G*ydnx)1{UiCfsQ=z8tKe=v_o}}ZX?v$@_cJ6u zv>Y(jQaKxWc=F9h2c0{3Z%-O94m`kntiR?^6uR=S=ogCiyc>h1gtR zsH3trD=Hkem&$_2hxI{PfOp`f2&b^zLwCwd16}t>Ea>wqAEA6sD?XB}_$~+==M!QA z^}F0wE#E@6!EC5b24eMYMeTvq*F9k#`=yO@>B9~T84*KircIkSgk8KMExrM)cy($# z`zm1s6Fxkkb{8Fpt=jfZkK^tLIO?zPn=RxC#yE(1_LTrbrSQwa_?1QBj$M^vEtFBF zl6&(){0P+A(#V=Y{>|Z~8>?=KHb4I&0`#-x+>*dw#I(^}c5lqsu94M3U>or?@?P-O zf860iCJ)^4tnWGK0z6Y0QTWA42eG|NKQaz)_zzy(`YYPyt$M))*gq==tZU8$vu!_N zYSliTsuy5g)Ve;44}y154lK|NQM`?fjkvb(j=hbW+6Ci7q%#N%L8<^mSrb`!DZ$$y z@nS;Wu}K8~*k*-YtF#G_!+)6b+wuoy|}PH2IFe86_u z`L|}Fn(ulUeY5QL)8pZJ|De+2Qn8U6Q7kJbQ7301FMb?X>F+lQ_INh>$-|{=t*-w@ z>8J7gF=|S%DoR0B*i!IrlJ{iZ2>vnZR>Kx4buQ$M*wd(x&3xm80Zqpt(W8O)slc`k ze+t#t0S?&GUp$s-M|D2X+xHqtn`oWB+=ud!Y}XbE=*69%v5D(aHF$ls+x4S^FrHBN zYphq@%*WIa*8Jilpl3L=E>q}hSXAVm1e@6e*O#-}QKb%o?)H1~eenSvn9jNgS>Lk( zKpLUT3!AvTcoq$wFKmZ<&i$x8=iTW>>IL?7WOB#kXZNdN)qWgXD{CSaWe-Pn=aCKI1sgk_{=W&!Dg|Q|K6?8vgW(D)|;}R1va`i zq2422o2g}MD^a7J!tlo^0Xynp5PV5$Oi@9hYq}Qp3PnIx^|ntfQ`QRXY3!_)ZhOik z{y$Et+^15AZqU76n-rTYSWISm*R#ViU6DTe@ov`FfX0GvWRmyy2tk!s3`Frs*PXGS z3lK8N`jNEsj`dZfV)Yb?EDPdHVU>LA6)ge1DP;bHySN9Osh=!zrUUz4XJWtMT(5s1 zyY=cg!0EXkLS9KbxWNn-0z-~f=IChNBG%XG5b2b2>&$b9mwvFeFLzvDwG-C3W0!+c zd~9|*rg>Up+$(CA&9?LpFejHrt;U%3oY4InncAUf%&W(=E&TnOnTpJ|pbs1$O7Bvf zjnXD=1O88d-%znjInk5?v%)mGjB5Qw2Wq9ml)TYqKV^onb}m0hR;yBFm(IGjfOq~z zlE^;;=jWAlgG1=cn=R~HTWgR5TafZ@zWBb*3PaT`zj(YQkgu`m4~-dy8;|bXbOU=> zh6cZ%bM(W$@ao4 zrkMcG^N_va=%|TC24ATnKkuUwGlSHIJjx>Jd&lbc496N@|0?7|6OlCJsnC|6-iiUH zCeww_#(QmIRpH2IJ;ZCiN7SE(nYSc@71S3W)r32Mk%{LM%foW zws*b8e!P}8u-n)7L!;g?&jJZMWfgon93NU4eq1;YrvAZx>Sf_*`Oj~hQ>jqB7V@=v zn1Mch^6djNGbsw~NncA~h?&~{3sZNQTf*4-b6n*-#fU=xe z+;}si%jAw0k%q+WH6A}xmo|{gUveHs<1p*}Zhmm4>>BWn8lw>`&GIfW@a?jrbORnI z57>}`BM?RZFIx%6k46>}d$bm?g*Qyj)QUvQEBXwXf`-v7_*#v=K6y((3Ft%bpQ)NZ z9)-0jc1dEj$|YA`3dJ1CE%!M1Ysn*j7qPI#2cbXFi=Z~Da4(>`Yj3=R@N9etG?UQS zR|!QTB z#^$|(3QiJ;-ph|!#?a6(=q&4Mz=SmoM@)2>v>e6vh(r?EZ9;D8tI{HoR^o8q=W(Yi z(vdbhIn?cm)`kXU?s}CcCO&*#7%!YO!$FQ)7&*3W5fl33gW^JDN@#gURta~9Yk55Z z+7;XC0hrJ>2U|%Cc+AL+uTeyPo4{J=vhK%)b|67b;G4D)WlQ7f!q_x+?4i z-M@QvJ%Oe80n#*T{T=5GNkT^C>P71Soaf7(JH759T7XI0GeA5sIlC&dc>p@A$*U_+sT^{BFUZiHsyg{ux9kC_wLBOIEi9^X=h?UbwRkyBGQ zmnlW4?CWgU)z#Jc7ha$=0}b<&xDm;DNpBIri2XgAB)+%6mmY}9h$N=#As_fpIY&at zvyYTl#G^a*d=0_ib)OSLN_QLwLT{A54ILHmsWv?Y+xt}H(0L{xc^qtWDL*S1>Ilmg z3hv?t2h#JMa{AeTH97QxgP;d_*6PZcg)6naUet$b*F60 zVeT=1;BY8N%`ib@Md?S+j(wt%VPR1Pq1FTD8ek5i4Roa>-253j7#`g{YoEADj82%b zNs~FafL`PWH5oUm?YpJ{uUFsAhB(qFB>ujNsv_G^zi8Yo*>>`{{K z7X9+$c4n8JB6@K=brrB?T-N}B2t0c|X8!n-HY%E@;6ZZ0{)oSgIR8P)yyIs`H`s;G zBL?gL+4AH%T5mjA46Lp>=Y}(T$%(r|usKfxHCe2~ZCpE+g8TmlIWZP92bFUT@`7zf zh^6nukOG6Ri^d6N8I|k*7C%Yy*nl>Vz6w@wF3w-$@c%1)NFgv>^zcLUz^pHJgM%MLC> zAIB?OO2u)W_tz_)BT1FjeQ$v8s7t&$nPbqjZYI0h4I=wB+WCL8IT+p!#G^c8Oa<}AJpHJ|SiO-QDZDvnX38vpw7Vg8oy zjZ-#%z4-*_=zim*!`&Hjb41{AAtok0-hWl|LdK0QEtl&r-q;K*W#${ay=>eyx|CW{ z{h_Y(wn=u~qS=^|L1#TJL?f1@EO)mOer%4A2{#7b%mx@XoMgKzTpf{l%c~$<(r)Jc zqnr0}GUM`Hd{Qf~BCbEvpwFEBfU0U z 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 Date: Thu, 5 Mar 2026 14:40:13 -0500 Subject: [PATCH 34/41] fix(helm): use fullname in selector labels to prevent mismatch on upgrade (#47) * fix(helm): use fullname in selector labels to prevent mismatch on upgrade Use kagent.fullname instead of kagent.name in selectorLabels so that changing nameOverride does not alter the app.kubernetes.io/name selector label. Deployment spec.selector.matchLabels is immutable in Kubernetes, so any label change causes a Service/Deployment selector mismatch after helm upgrade, leaving the Service with zero endpoints. With this fix, both the old config (fullnameOverride: kagent-tools) and the new config (nameOverride: tools) resolve to the same fullname "kagent-tools" for the default release name, keeping selectors stable across upgrades. Fixes kagent-dev/kagent#1427 Signed-off-by: Jaison Paul * fix(e2e): update label selectors to match fullname-based selector labels Update E2E tests to use app.kubernetes.io/instance label selector instead of app.kubernetes.io/name since the PR changes selectorLabels to use kagent.fullname. The fullname template returns the release name (kagent-tools-e2e), so the tests now use app.kubernetes.io/instance= which remains stable and matches the updated selector labels in the Helm chart. This fixes the E2E test failures where pods weren't being found because the label selector no longer matched after the selectorLabels change.Signed-off-by: Eitan Yarmush --------- Signed-off-by: Jaison Paul Signed-off-by: Eitan Yarmush Co-authored-by: Eitan Yarmush --- helm/kagent-tools/templates/_helpers.tpl | 2 +- helm/kagent-tools/tests/deployment_test.yaml | 2 +- test/e2e/helpers_test.go | 2 +- test/e2e/k8s_test.go | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/helm/kagent-tools/templates/_helpers.tpl b/helm/kagent-tools/templates/_helpers.tpl index d47a01e2..a555da5e 100644 --- a/helm/kagent-tools/templates/_helpers.tpl +++ b/helm/kagent-tools/templates/_helpers.tpl @@ -43,7 +43,7 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} Selector labels */}} {{- define "kagent.selectorLabels" -}} -app.kubernetes.io/name: {{ include "kagent.name" . }} +app.kubernetes.io/name: {{ include "kagent.fullname" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} diff --git a/helm/kagent-tools/tests/deployment_test.yaml b/helm/kagent-tools/tests/deployment_test.yaml index 0e4e8cac..84bdaae0 100644 --- a/helm/kagent-tools/tests/deployment_test.yaml +++ b/helm/kagent-tools/tests/deployment_test.yaml @@ -171,5 +171,5 @@ tests: - equal: path: spec.template.spec.topologySpreadConstraints[0].labelSelector.matchLabels value: - app.kubernetes.io/name: kagent-tools + app.kubernetes.io/name: RELEASE-NAME app.kubernetes.io/instance: RELEASE-NAME diff --git a/test/e2e/helpers_test.go b/test/e2e/helpers_test.go index f88b6aba..8f6d5221 100644 --- a/test/e2e/helpers_test.go +++ b/test/e2e/helpers_test.go @@ -224,7 +224,7 @@ func InstallKAgentTools(namespace string, releaseName string) { defer cancel() output, err := commands.NewCommandBuilder("kubectl"). - WithArgs("get", "pods", "-n", namespace, "-l", "app.kubernetes.io/name=kagent-tools", "-o", "jsonpath={.items[*].status.phase}"). + WithArgs("get", "pods", "-n", namespace, "-l", "app.kubernetes.io/instance="+releaseName, "-o", "jsonpath={.items[*].status.phase}"). Execute(ctx) if err != nil { diff --git a/test/e2e/k8s_test.go b/test/e2e/k8s_test.go index 65ad66ac..e90b6ddb 100644 --- a/test/e2e/k8s_test.go +++ b/test/e2e/k8s_test.go @@ -56,7 +56,7 @@ var _ = Describe("KAgent Tools Kubernetes E2E Tests", Ordered, func() { log.Info("Checking if kagent-tools pods are running", "namespace", namespace) output, err := commands.NewCommandBuilder("kubectl"). - WithArgs("get", "pods", "-n", namespace, "-l", "app.kubernetes.io/name=kagent-tools", "-o", "json"). + WithArgs("get", "pods", "-n", namespace, "-l", "app.kubernetes.io/instance="+releaseName, "-o", "json"). Execute(ctx) Expect(err).ToNot(HaveOccurred()) @@ -70,7 +70,7 @@ var _ = Describe("KAgent Tools Kubernetes E2E Tests", Ordered, func() { log.Info("Checking if kagent-tools service is accessible", "namespace", namespace) output, err := commands.NewCommandBuilder("kubectl"). - WithArgs("get", "svc", "-n", namespace, "-l", "app.kubernetes.io/name=kagent-tools", "-o", "json"). + WithArgs("get", "svc", "-n", namespace, "-l", "app.kubernetes.io/instance="+releaseName, "-o", "json"). Execute(ctx) Expect(err).ToNot(HaveOccurred()) From 441d76713a555057b21f1d9247fa715012057f98 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Thu, 5 Mar 2026 17:33:33 -0500 Subject: [PATCH 35/41] fix(helm): rename helpers to avoid parent chart collision (#49) Renames all helper templates from kagent.* to kagent-tools.* prefix to prevent naming conflicts with the parent kagent chart. When Helm renders subcharts, template definitions are global, causing the parent chart's helpers to override the subchart's helpers with the same names. This fixes: - Selector label mismatch when using nameOverride (was using parent's logic instead of subchart's fullname logic) - Helm upgrade failures due to immutable selector field changes - Enables proper use of nameOverride instead of requiring fullnameOverride workaround All helper references updated across all template files: - _helpers.tpl: Renamed 10 helper definitions - deployment.yaml, service.yaml, serviceaccount.yaml: Updated references - clusterrole.yaml, clusterrolebinding.yaml: Updated references - servicemonitor.yaml, NOTES.txt: Updated references Backward compatible: existing fullnameOverride usage continues to work. Signed-off-by: Eitan Yarmush --- helm/kagent-tools/templates/NOTES.txt | 2 +- helm/kagent-tools/templates/_helpers.tpl | 28 +++++++++---------- helm/kagent-tools/templates/clusterrole.yaml | 8 +++--- .../templates/clusterrolebinding.yaml | 14 +++++----- helm/kagent-tools/templates/deployment.yaml | 16 +++++------ helm/kagent-tools/templates/service.yaml | 16 +++++------ .../templates/serviceaccount.yaml | 6 ++-- .../templates/servicemonitor.yaml | 8 +++--- 8 files changed, 49 insertions(+), 49 deletions(-) diff --git a/helm/kagent-tools/templates/NOTES.txt b/helm/kagent-tools/templates/NOTES.txt index a5be260b..2876aa1e 100644 --- a/helm/kagent-tools/templates/NOTES.txt +++ b/helm/kagent-tools/templates/NOTES.txt @@ -3,7 +3,7 @@ # This is a Helm chart for Kagent Tools # # 1. Forward application port by running these commands in the terminal: -# kubectl -n {{ include "kagent.namespace" . }} port-forward service/{{ .Release.Name }} {{.Values.service.ports.tools.targetPort}}:{{.Values.service.ports.tools.port}} & +# kubectl -n {{ include "kagent-tools.namespace" . }} port-forward service/{{ .Release.Name }} {{.Values.service.ports.tools.targetPort}}:{{.Values.service.ports.tools.port}} & # # 2. Then visit http://127.0.0.1:{{.Values.service.ports.tools.port}}/mcp to use MCP # diff --git a/helm/kagent-tools/templates/_helpers.tpl b/helm/kagent-tools/templates/_helpers.tpl index a555da5e..40158ae0 100644 --- a/helm/kagent-tools/templates/_helpers.tpl +++ b/helm/kagent-tools/templates/_helpers.tpl @@ -1,14 +1,14 @@ {{/* Expand the name of the chart. */}} -{{- define "kagent.name" -}} +{{- define "kagent-tools.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. */}} -{{- define "kagent.fullname" -}} +{{- define "kagent-tools.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} @@ -23,16 +23,16 @@ Create a default fully qualified app name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "kagent.chart" -}} +{{- define "kagent-tools.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "kagent.labels" -}} -helm.sh/chart: {{ include "kagent.chart" . }} -{{ include "kagent.selectorLabels" . }} +{{- define "kagent-tools.labels" -}} +helm.sh/chart: {{ include "kagent-tools.chart" . }} +{{ include "kagent-tools.selectorLabels" . }} {{- if .Chart.Version }} app.kubernetes.io/version: {{ .Chart.Version | quote }} {{- end }} @@ -42,18 +42,18 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Selector labels */}} -{{- define "kagent.selectorLabels" -}} -app.kubernetes.io/name: {{ include "kagent.fullname" . }} +{{- define "kagent-tools.selectorLabels" -}} +app.kubernetes.io/name: {{ include "kagent-tools.fullname" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/*Default provider name*/}} -{{- define "kagent.defaultProviderName" -}} +{{- define "kagent-tools.defaultProviderName" -}} {{ .Values.providers.default | default "openAI" | lower}} {{- end }} {{/*Default model name*/}} -{{- define "kagent.defaultModelConfigName" -}} +{{- define "kagent-tools.defaultModelConfigName" -}} default-model-config {{- end }} @@ -61,22 +61,22 @@ default-model-config Expand the namespace of the release. Allows overriding it for multi-namespace deployments in combined charts. */}} -{{- define "kagent.namespace" -}} +{{- define "kagent-tools.namespace" -}} {{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} {{- end }} {{/* Service account name: default when useDefaultServiceAccount is true, otherwise the chart fullname. */}} -{{- define "kagent.serviceAccountName" -}} -{{- if .Values.useDefaultServiceAccount }}default{{- else }}{{ include "kagent.fullname" . }}{{- end }} +{{- define "kagent-tools.serviceAccountName" -}} +{{- if .Values.useDefaultServiceAccount }}default{{- else }}{{ include "kagent-tools.fullname" . }}{{- end }} {{- end }} {{/* Watch namespaces - transforms list of namespaces cached by the controller into comma-separated string Removes duplicates */}} -{{- define "kagent.watchNamespaces" -}} +{{- define "kagent-tools.watchNamespaces" -}} {{- $nsSet := dict }} {{- .Values.controller.watchNamespaces | default list | uniq | join "," }} {{- end -}} diff --git a/helm/kagent-tools/templates/clusterrole.yaml b/helm/kagent-tools/templates/clusterrole.yaml index bb6e654f..cdbb2931 100644 --- a/helm/kagent-tools/templates/clusterrole.yaml +++ b/helm/kagent-tools/templates/clusterrole.yaml @@ -3,9 +3,9 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ include "kagent.fullname" . }}-read-role + name: {{ include "kagent-tools.fullname" . }}-read-role labels: - {{- include "kagent.labels" . | nindent 4 }} + {{- include "kagent-tools.labels" . | nindent 4 }} rules: # Core workload resources - apiGroups: [""] @@ -82,9 +82,9 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ include "kagent.fullname" . }}-cluster-admin-role + name: {{ include "kagent-tools.fullname" . }}-cluster-admin-role labels: - {{- include "kagent.labels" . | nindent 4 }} + {{- include "kagent-tools.labels" . | nindent 4 }} rules: - apiGroups: ["*"] resources: ["*"] diff --git a/helm/kagent-tools/templates/clusterrolebinding.yaml b/helm/kagent-tools/templates/clusterrolebinding.yaml index b8503bec..3c9fe3f2 100644 --- a/helm/kagent-tools/templates/clusterrolebinding.yaml +++ b/helm/kagent-tools/templates/clusterrolebinding.yaml @@ -3,22 +3,22 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: {{- if .Values.rbac.readOnly }} - name: {{ include "kagent.fullname" . }}-read-rolebinding + name: {{ include "kagent-tools.fullname" . }}-read-rolebinding {{- else }} - name: {{ include "kagent.fullname" . }}-cluster-admin-rolebinding + name: {{ include "kagent-tools.fullname" . }}-cluster-admin-rolebinding {{- end }} labels: - {{- include "kagent.labels" . | nindent 4 }} + {{- include "kagent-tools.labels" . | nindent 4 }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole {{- if .Values.rbac.readOnly }} - name: {{ include "kagent.fullname" . }}-read-role + name: {{ include "kagent-tools.fullname" . }}-read-role {{- else }} - name: {{ include "kagent.fullname" . }}-cluster-admin-role + name: {{ include "kagent-tools.fullname" . }}-cluster-admin-role {{- end }} subjects: - kind: ServiceAccount - name: {{ include "kagent.fullname" . }} - namespace: {{ include "kagent.namespace" . }} + name: {{ include "kagent-tools.fullname" . }} + namespace: {{ include "kagent-tools.namespace" . }} {{- end }} diff --git a/helm/kagent-tools/templates/deployment.yaml b/helm/kagent-tools/templates/deployment.yaml index fca3bb6f..a78779cd 100644 --- a/helm/kagent-tools/templates/deployment.yaml +++ b/helm/kagent-tools/templates/deployment.yaml @@ -1,15 +1,15 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "kagent.fullname" . }} - namespace: {{ include "kagent.namespace" . }} + name: {{ include "kagent-tools.fullname" . }} + namespace: {{ include "kagent-tools.namespace" . }} labels: - {{- include "kagent.labels" . | nindent 4 }} + {{- include "kagent-tools.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: - {{- include "kagent.selectorLabels" . | nindent 6 }} + {{- include "kagent-tools.selectorLabels" . | nindent 6 }} template: metadata: {{- with .Values.podAnnotations }} @@ -17,7 +17,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} labels: - {{- include "kagent.selectorLabels" . | nindent 8 }} + {{- include "kagent-tools.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: @@ -43,7 +43,7 @@ spec: {{- if not (hasKey . "labelSelector") }} labelSelector: matchLabels: - {{- include "kagent.selectorLabels" $ | nindent 14 }} + {{- include "kagent-tools.selectorLabels" $ | nindent 14 }} {{- end }} {{- end }} {{- end }} @@ -51,7 +51,7 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} - serviceAccountName: {{ include "kagent.serviceAccountName" . }} + serviceAccountName: {{ include "kagent-tools.serviceAccountName" . }} containers: - name: tools command: @@ -82,7 +82,7 @@ spec: - name: OPENAI_API_KEY valueFrom: secretKeyRef: - name: {{ include "kagent.fullname" . }}-openai + name: {{ include "kagent-tools.fullname" . }}-openai key: OPENAI_API_KEY optional: true # if the secret is not found, the tool will not be available - name: OTEL_TRACING_ENABLED diff --git a/helm/kagent-tools/templates/service.yaml b/helm/kagent-tools/templates/service.yaml index f578670e..06bcb67d 100644 --- a/helm/kagent-tools/templates/service.yaml +++ b/helm/kagent-tools/templates/service.yaml @@ -1,10 +1,10 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "kagent.fullname" . }} - namespace: {{ include "kagent.namespace" . }} + name: {{ include "kagent-tools.fullname" . }} + namespace: {{ include "kagent-tools.namespace" . }} labels: - {{- include "kagent.labels" . | nindent 4 }} + {{- include "kagent-tools.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: @@ -18,20 +18,20 @@ spec: protocol: TCP name: tools selector: - {{- include "kagent.selectorLabels" . | nindent 4 }} + {{- include "kagent-tools.selectorLabels" . | nindent 4 }} --- apiVersion: v1 kind: Service metadata: - name: {{ include "kagent.fullname" . }}-metrics - namespace: {{ include "kagent.namespace" . }} + name: {{ include "kagent-tools.fullname" . }}-metrics + namespace: {{ include "kagent-tools.namespace" . }} labels: - {{- include "kagent.labels" . | nindent 4 }} + {{- include "kagent-tools.labels" . | nindent 4 }} app.kubernetes.io/component: metrics spec: selector: - {{- include "kagent.selectorLabels" . | nindent 4 }} + {{- include "kagent-tools.selectorLabels" . | nindent 4 }} ports: - name: prometheus-metrics protocol: TCP diff --git a/helm/kagent-tools/templates/serviceaccount.yaml b/helm/kagent-tools/templates/serviceaccount.yaml index 3422acb5..da39344e 100644 --- a/helm/kagent-tools/templates/serviceaccount.yaml +++ b/helm/kagent-tools/templates/serviceaccount.yaml @@ -2,8 +2,8 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ include "kagent.fullname" . }} - namespace: {{ include "kagent.namespace" . }} + name: {{ include "kagent-tools.fullname" . }} + namespace: {{ include "kagent-tools.namespace" . }} labels: - {{- include "kagent.labels" . | nindent 4 }} + {{- include "kagent-tools.labels" . | nindent 4 }} {{- end }} diff --git a/helm/kagent-tools/templates/servicemonitor.yaml b/helm/kagent-tools/templates/servicemonitor.yaml index ded05cdb..c13c5d82 100644 --- a/helm/kagent-tools/templates/servicemonitor.yaml +++ b/helm/kagent-tools/templates/servicemonitor.yaml @@ -3,18 +3,18 @@ apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: - name: {{ include "kagent.fullname" . }} - namespace: {{ include "kagent.namespace" . }} + name: {{ include "kagent-tools.fullname" . }} + namespace: {{ include "kagent-tools.namespace" . }} labels: {{- toYaml .Values.tools.metrics.servicemonitor.labels | nindent 4 }} spec: selector: matchLabels: - {{- include "kagent.selectorLabels" . | nindent 6 }} + {{- include "kagent-tools.selectorLabels" . | nindent 6 }} app.kubernetes.io/component: metrics namespaceSelector: matchNames: - - {{ include "kagent.namespace" . }} + - {{ include "kagent-tools.namespace" . }} endpoints: - port: prometheus-metrics interval: {{ .Values.tools.metrics.servicemonitor.interval | default "30s" }} From 7b7cdc39b619d91a2e070ac617aa8d54060d26db Mon Sep 17 00:00:00 2001 From: Felipe Vicens Date: Fri, 6 Mar 2026 14:58:08 +0100 Subject: [PATCH 36/41] feat(k8s): add k8s_patch_status tool for patching resource status subresource (#50) Signed-off-by: Felipe Vicens --- pkg/k8s/k8s.go | 49 ++++++++++++++++++++++++++++++++++++++++++++ pkg/k8s/k8s_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index 22a8badf..f84a0471 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -157,6 +157,47 @@ func (k *K8sTool) handlePatchResource(ctx context.Context, request mcp.CallToolR return k.runKubectlCommandWithCacheInvalidation(ctx, request.Header, args...) } +// Patch resource status +func (k *K8sTool) handlePatchStatus(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + resourceType := mcp.ParseString(request, "resource_type", "") + resourceName := mcp.ParseString(request, "resource_name", "") + patch := mcp.ParseString(request, "patch", "") + namespace := mcp.ParseString(request, "namespace", "default") + + if resourceType == "" || resourceName == "" || patch == "" { + return mcp.NewToolResultError("resource_type, resource_name, and patch parameters are required"), nil + } + + // Validate resource name for security + if err := security.ValidateK8sResourceName(resourceName); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid resource name: %v", err)), nil + } + + // Validate namespace for security + if err := security.ValidateNamespace(namespace); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid namespace: %v", err)), nil + } + + // Validate patch content as JSON/YAML + if err := security.ValidateYAMLContent(patch); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("Invalid patch content: %v", err)), nil + } + + args := []string{ + "patch", + resourceType, + resourceName, + "--subresource=status", + "--type=merge", + "-p", + patch, + "-n", + namespace, + } + + return k.runKubectlCommandWithCacheInvalidation(ctx, request.Header, args...) +} + // Apply manifest from content func (k *K8sTool) handleApplyManifest(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { manifest := mcp.ParseString(request, "manifest", "") @@ -683,6 +724,14 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string, readO mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_patch_resource", k8sTool.handlePatchResource))) + s.AddTool(mcp.NewTool("k8s_patch_status", + mcp.WithDescription("Patch the status of a Kubernetes resource"), + mcp.WithString("resource_type", mcp.Description("Type of resource (deployment, service, etc.)"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), + mcp.WithString("patch", mcp.Description("JSON/YAML status patch"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_patch_status", k8sTool.handlePatchStatus))) + s.AddTool(mcp.NewTool("k8s_apply_manifest", mcp.WithDescription("Apply a YAML manifest to the Kubernetes cluster"), mcp.WithString("manifest", mcp.Description("YAML manifest content"), mcp.Required()), diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go index 8ac03409..44df8a92 100644 --- a/pkg/k8s/k8s_test.go +++ b/pkg/k8s/k8s_test.go @@ -264,6 +264,56 @@ func TestHandlePatchResource(t *testing.T) { }) } +func TestHandlePatchStatus(t *testing.T) { + ctx := context.Background() + + t.Run("missing parameters", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + ctx := cmd.WithShellExecutor(context.Background(), mock) + + k8sTool := newTestK8sTool() + + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "customresource", + // Missing resource_name and patch + } + + result, err := k8sTool.handlePatchStatus(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + + // Verify no commands were executed since parameters are missing + callLog := mock.GetCallLog() + assert.Len(t, callLog, 0) + }) + + t.Run("valid parameters", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `customresource.kagent.dev/test-resource patched` + mock.AddCommandString("kubectl", []string{"patch", "customresource", "test-resource", "--subresource=status", "--type=merge", "-p", `{"status":{"phase":"Ready"}}`, "-n", "default"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "resource_type": "customresource", + "resource_name": "test-resource", + "patch": `{"status":{"phase":"Ready"}}`, + } + + result, err := k8sTool.handlePatchStatus(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + resultText := getResultText(result) + assert.Contains(t, resultText, "patched") + }) +} + func TestHandleDeleteResource(t *testing.T) { ctx := context.Background() From 4fe98bec275b29461c5b02648fdd188bb643145f Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Thu, 19 Mar 2026 09:05:56 -0400 Subject: [PATCH 37/41] fix(security): bump grpc and CLI tool versions to resolve CVEs (#52) Bump google.golang.org/grpc v1.78.0 -> v1.79.3 to fix CRITICAL CVE-2026-33186 (authorization bypass). Bump all bundled CLI tools to latest releases (kubectl 1.35.3, helm 4.1.3, istioctl 1.28.5, argo-rollouts 1.8.4, cilium 0.19.2) to reduce CVE surface area. Signed-off-by: Eitan Yarmush --- .github/workflows/ci.yaml | 4 ++-- Makefile | 10 +++++----- go.mod | 4 ++-- go.sum | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dd105ca4..819a56eb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,7 +47,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '^1.25.6' + go-version: '^1.26.1' cache: false - name: Run cmd/main.go tests @@ -64,7 +64,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '^1.25.5' + go-version: '^1.26.1' cache: false - name: Create k8s Kind Cluster diff --git a/Makefile b/Makefile index 4b188f15..b77e1eea 100644 --- a/Makefile +++ b/Makefile @@ -136,11 +136,11 @@ DOCKER_BUILDER ?= docker buildx DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUILDX_BUILDER_NAME) # tools image build args -TOOLS_ISTIO_VERSION ?= 1.28.3 -TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.3 -TOOLS_KUBECTL_VERSION ?= 1.35.1 -TOOLS_HELM_VERSION ?= 4.1.1 -TOOLS_CILIUM_VERSION ?= 0.19.0 +TOOLS_ISTIO_VERSION ?= 1.28.5 +TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.4 +TOOLS_KUBECTL_VERSION ?= 1.35.3 +TOOLS_HELM_VERSION ?= 4.1.3 +TOOLS_CILIUM_VERSION ?= 0.19.2 # build args TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION) diff --git a/go.mod b/go.mod index e796d121..5f1e7439 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/kagent-dev/tools -go 1.25.6 +go 1.26.1 require ( github.com/joho/godotenv v1.5.1 @@ -187,7 +187,7 @@ require ( golang.org/x/tools v0.42.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.78.0 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 2411c567..8e473845 100644 --- a/go.sum +++ b/go.sum @@ -1254,8 +1254,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From e8c0b7c2a224d3c55f13e621aa0ea7f53dbaf319 Mon Sep 17 00:00:00 2001 From: Jet Chiang Date: Fri, 27 Mar 2026 19:42:46 -0400 Subject: [PATCH 38/41] feat(helm): support namespaced RBAC for tools (#53) * namespaced rbac Signed-off-by: Jet Chiang * oops forgot i renamed it Signed-off-by: Jet Chiang --------- Signed-off-by: Jet Chiang --- helm/kagent-tools/templates/clusterrole.yaml | 175 ++++++++++-------- .../templates/clusterrolebinding.yaml | 33 ++++ helm/kagent-tools/tests/rbac_test.yaml | 66 +++++++ helm/kagent-tools/values.yaml | 6 + 4 files changed, 204 insertions(+), 76 deletions(-) create mode 100644 helm/kagent-tools/tests/rbac_test.yaml diff --git a/helm/kagent-tools/templates/clusterrole.yaml b/helm/kagent-tools/templates/clusterrole.yaml index cdbb2931..48b615b9 100644 --- a/helm/kagent-tools/templates/clusterrole.yaml +++ b/helm/kagent-tools/templates/clusterrole.yaml @@ -1,95 +1,118 @@ -{{- if and (not .Values.useDefaultServiceAccount) .Values.rbac.create }} +{{- define "kagent-tools.rules" -}} {{- if .Values.rbac.readOnly }} -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: {{ include "kagent-tools.fullname" . }}-read-role - labels: - {{- include "kagent-tools.labels" . | nindent 4 }} -rules: - # Core workload resources - - apiGroups: [""] - resources: - - pods - - services - - endpoints - - configmaps - - serviceaccounts - - persistentvolumeclaims - - replicationcontrollers - - namespaces - verbs: ["get", "list", "watch"] +# Core workload resources +- apiGroups: [""] + resources: + - pods + - services + - endpoints + - configmaps + - serviceaccounts + - persistentvolumeclaims + - replicationcontrollers + - namespaces + verbs: ["get", "list", "watch"] - # Pod logs (subresource) - - apiGroups: [""] - resources: - - pods/log - verbs: ["get", "list"] +# Pod logs (subresource) +- apiGroups: [""] + resources: + - pods/log + verbs: ["get", "list"] - # Events - - apiGroups: [""] - resources: - - events - verbs: ["get", "list", "watch"] - - apiGroups: ["events.k8s.io"] - resources: - - events - verbs: ["get", "list", "watch"] +# Events +- apiGroups: [""] + resources: + - events + verbs: ["get", "list", "watch"] +- apiGroups: ["events.k8s.io"] + resources: + - events + verbs: ["get", "list", "watch"] - # Apps workloads - - apiGroups: ["apps"] - resources: - - deployments - - statefulsets - - daemonsets - - replicasets - verbs: ["get", "list", "watch"] +# Apps workloads +- apiGroups: ["apps"] + resources: + - deployments + - statefulsets + - daemonsets + - replicasets + verbs: ["get", "list", "watch"] - # Batch workloads - - apiGroups: ["batch"] - resources: - - jobs - - cronjobs - verbs: ["get", "list", "watch"] +# Batch workloads +- apiGroups: ["batch"] + resources: + - jobs + - cronjobs + verbs: ["get", "list", "watch"] - # Networking - - apiGroups: ["networking.k8s.io"] - resources: - - ingresses - - networkpolicies - verbs: ["get", "list", "watch"] +# Networking +- apiGroups: ["networking.k8s.io"] + resources: + - ingresses + - networkpolicies + verbs: ["get", "list", "watch"] - # Autoscaling - - apiGroups: ["autoscaling"] - resources: - - horizontalpodautoscalers - verbs: ["get", "list", "watch"] - - {{- if .Values.rbac.allowSecrets }} - # Secrets (opt-in) - - apiGroups: [""] - resources: - - secrets - verbs: ["get", "list", "watch"] - {{- end }} +# Autoscaling +- apiGroups: ["autoscaling"] + resources: + - horizontalpodautoscalers + verbs: ["get", "list", "watch"] - {{- with .Values.rbac.additionalRules }} - # Additional user-defined rules - {{- toYaml . | nindent 2 }} - {{- end }} +{{- if .Values.rbac.allowSecrets }} +# Secrets (opt-in) +- apiGroups: [""] + resources: + - secrets + verbs: ["get", "list", "watch"] +{{- end }} +{{- with .Values.rbac.additionalRules }} +# Additional user-defined rules +{{- toYaml . | nindent 0 }} +{{- end }} {{- else }} +- apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] +{{- if .Values.rbac.clusterScoped }} +- nonResourceURLs: ["*"] + verbs: ["*"] +{{- end }} +{{- end }} +{{- end -}} + +{{- if and (not .Values.useDefaultServiceAccount) .Values.rbac.create }} +{{- if .Values.rbac.clusterScoped }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: + {{- if .Values.rbac.readOnly }} + name: {{ include "kagent-tools.fullname" . }}-read-role + {{- else }} name: {{ include "kagent-tools.fullname" . }}-cluster-admin-role + {{- end }} labels: {{- include "kagent-tools.labels" . | nindent 4 }} rules: -- apiGroups: ["*"] - resources: ["*"] - verbs: ["*"] -- nonResourceURLs: ["*"] - verbs: ["*"] + {{- include "kagent-tools.rules" . | nindent 2 }} + +{{- else }} +{{- $namespaces := .Values.rbac.namespaces | default (list (include "kagent-tools.namespace" .)) }} +{{- range $namespace := $namespaces }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + {{- if $.Values.rbac.readOnly }} + name: {{ include "kagent-tools.fullname" $ }}-read-role + {{- else }} + name: {{ include "kagent-tools.fullname" $ }}-cluster-admin-role + {{- end }} + namespace: {{ $namespace }} + labels: + {{- include "kagent-tools.labels" $ | nindent 4 }} +rules: + {{- include "kagent-tools.rules" $ | nindent 2 }} +{{- end }} {{- end }} {{- end }} diff --git a/helm/kagent-tools/templates/clusterrolebinding.yaml b/helm/kagent-tools/templates/clusterrolebinding.yaml index 3c9fe3f2..d6671a22 100644 --- a/helm/kagent-tools/templates/clusterrolebinding.yaml +++ b/helm/kagent-tools/templates/clusterrolebinding.yaml @@ -1,4 +1,6 @@ {{- if and (not .Values.useDefaultServiceAccount) .Values.rbac.create }} + +{{- if .Values.rbac.clusterScoped }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: @@ -21,4 +23,35 @@ subjects: - kind: ServiceAccount name: {{ include "kagent-tools.fullname" . }} namespace: {{ include "kagent-tools.namespace" . }} + +{{- else }} +{{- $namespaces := .Values.rbac.namespaces | default (list (include "kagent-tools.namespace" .)) }} +{{- range $namespace := $namespaces }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + {{- if $.Values.rbac.readOnly }} + name: {{ include "kagent-tools.fullname" $ }}-read-rolebinding + {{- else }} + name: {{ include "kagent-tools.fullname" $ }}-cluster-admin-rolebinding + {{- end }} + namespace: {{ $namespace }} + labels: + {{- include "kagent-tools.labels" $ | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + {{- if $.Values.rbac.readOnly }} + name: {{ include "kagent-tools.fullname" $ }}-read-role + {{- else }} + name: {{ include "kagent-tools.fullname" $ }}-cluster-admin-role + {{- end }} +subjects: +- kind: ServiceAccount + name: {{ include "kagent-tools.fullname" $ }} + namespace: {{ include "kagent-tools.namespace" $ }} +{{- end }} +{{- end }} + {{- end }} diff --git a/helm/kagent-tools/tests/rbac_test.yaml b/helm/kagent-tools/tests/rbac_test.yaml new file mode 100644 index 00000000..15d05ca1 --- /dev/null +++ b/helm/kagent-tools/tests/rbac_test.yaml @@ -0,0 +1,66 @@ +suite: test rbac +templates: + - clusterrole.yaml + - clusterrolebinding.yaml +tests: + - it: should render clusterroles by default + asserts: + - isKind: + of: ClusterRole + template: clusterrole.yaml + - isKind: + of: ClusterRoleBinding + template: clusterrolebinding.yaml + + - it: should render Roles when clusterScoped is false + set: + rbac.clusterScoped: false + asserts: + - isKind: + of: Role + template: clusterrole.yaml + - isKind: + of: RoleBinding + template: clusterrolebinding.yaml + - equal: + path: metadata.namespace + value: NAMESPACE + template: clusterrole.yaml + - equal: + path: metadata.namespace + value: NAMESPACE + template: clusterrolebinding.yaml + + - it: should render multiple roles and bindings when namespaces are specified and clusterScoped is false + set: + rbac.clusterScoped: false + rbac.namespaces: + - ns1 + - ns2 + asserts: + - hasDocuments: + count: 2 + template: clusterrole.yaml + - equal: + path: metadata.namespace + value: ns1 + template: clusterrole.yaml + documentIndex: 0 + - equal: + path: metadata.namespace + value: ns2 + template: clusterrole.yaml + documentIndex: 1 + - hasDocuments: + count: 2 + template: clusterrolebinding.yaml + - equal: + path: metadata.namespace + value: ns1 + template: clusterrolebinding.yaml + documentIndex: 0 + - equal: + path: metadata.namespace + value: ns2 + template: clusterrolebinding.yaml + documentIndex: 1 diff --git a/helm/kagent-tools/values.yaml b/helm/kagent-tools/values.yaml index 90050e9b..2246cf93 100644 --- a/helm/kagent-tools/values.yaml +++ b/helm/kagent-tools/values.yaml @@ -104,6 +104,12 @@ rbac: # When false, no ClusterRole or ClusterRoleBinding are created. # The ServiceAccount is still created allowing you to attach your own roles externally. create: true + # -- If true, creates ClusterRole and ClusterRoleBinding resources. + # If false, creates Role and RoleBinding resources instead. + clusterScoped: true + # -- When clusterScoped is false, specify additional namespaces to create Roles and RoleBindings in. + # If empty, defaults to the release namespace. + namespaces: [] # When true, deploys a read-only ClusterRole (get, list, watch) instead of cluster-admin. # Pairs well with the --read-only CLI flag which disables write operations at the application layer. # Only applies when rbac.create is true. From 9694b512a86a2dcb0c084801975a2c0a59d9eb7a Mon Sep 17 00:00:00 2001 From: Dmytro Rashko Date: Wed, 1 Apr 2026 15:16:18 +0200 Subject: [PATCH 39/41] Fix incorrect cillium-dbg subcommands (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dmytro Rashko * Fix incorrect cilium-dbg subcommands * Bump outdated tools: - Argo Rollouts: 1.8.4 → 1.9.0 - Istio: 1.28.5 → 1.29.1 --- Makefile | 4 +- pkg/cilium/cilium.go | 24 ++- pkg/cilium/cilium_test.go | 352 +++++++++++++++++++++++++++++++ scripts/cilium/install-cilium.sh | 52 +++++ scripts/cilium/test-mcp-tools.sh | 96 +++++++++ 5 files changed, 515 insertions(+), 13 deletions(-) create mode 100755 scripts/cilium/install-cilium.sh create mode 100755 scripts/cilium/test-mcp-tools.sh diff --git a/Makefile b/Makefile index b77e1eea..dde2eea2 100644 --- a/Makefile +++ b/Makefile @@ -136,8 +136,8 @@ DOCKER_BUILDER ?= docker buildx DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUILDX_BUILDER_NAME) # tools image build args -TOOLS_ISTIO_VERSION ?= 1.28.5 -TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.8.4 +TOOLS_ISTIO_VERSION ?= 1.29.1 +TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.9.0 TOOLS_KUBECTL_VERSION ?= 1.35.3 TOOLS_HELM_VERSION ?= 4.1.3 TOOLS_CILIUM_VERSION ?= 0.19.2 diff --git a/pkg/cilium/cilium.go b/pkg/cilium/cilium.go index 9479378c..b92a8f1c 100644 --- a/pkg/cilium/cilium.go +++ b/pkg/cilium/cilium.go @@ -3,6 +3,7 @@ package cilium import ( "context" "fmt" + "strings" "github.com/kagent-dev/tools/internal/commands" "github.com/kagent-dev/tools/internal/telemetry" @@ -619,7 +620,8 @@ func runCiliumDbgCommandWithContext(ctx context.Context, command, nodeName strin if err != nil { return "", err } - args := []string{"exec", "-it", podName, "--", "cilium-dbg", command} + args := []string{"exec", "-n", "kube-system", podName, "--", "cilium-dbg"} + args = append(args, strings.Fields(command)...) kubeconfigPath := utils.GetKubeconfig() return commands.NewCommandBuilder("kubectl"). WithArgs(args...). @@ -780,13 +782,13 @@ func handleShowConfigurationOptions(ctx context.Context, request mcp.CallToolReq var cmd string if listAll { - cmd = "endpoint config --all" + cmd = "config --all" } else if listReadOnly { - cmd = "endpoint config -r" + cmd = "config -r" } else if listOptions { - cmd = "endpoint config --list-options" + cmd = "config --list-options" } else { - cmd = "endpoint config" + cmd = "config" } output, err := runCiliumDbgCommand(ctx, cmd, nodeName) @@ -810,7 +812,7 @@ func handleToggleConfigurationOption(ctx context.Context, request mcp.CallToolRe valueStr = "disable" } - cmd := fmt.Sprintf("endpoint config %s=%s", option, valueStr) + cmd := fmt.Sprintf("config %s=%s", option, valueStr) output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to toggle configuration option: %v", err)), nil @@ -885,7 +887,7 @@ func handleFQDNCache(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal func handleShowDNSNames(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand(ctx, "dns names", nodeName) + output, err := runCiliumDbgCommand(ctx, "fqdn names", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to show DNS names: %v", err)), nil } @@ -1000,7 +1002,7 @@ func handleListBPFMapEvents(ctx context.Context, request mcp.CallToolRequest) (* return mcp.NewToolResultError("map_name parameter is required"), nil } - cmd := fmt.Sprintf("bpf map events %s", mapName) + cmd := fmt.Sprintf("map events %s", mapName) output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list BPF map events: %v", err)), nil @@ -1016,7 +1018,7 @@ func handleGetBPFMap(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal return mcp.NewToolResultError("map_name parameter is required"), nil } - cmd := fmt.Sprintf("bpf map get %s", mapName) + cmd := fmt.Sprintf("map get %s", mapName) output, err := runCiliumDbgCommand(ctx, cmd, nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to get BPF map: %v", err)), nil @@ -1027,7 +1029,7 @@ func handleGetBPFMap(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal func handleListBPFMaps(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand(ctx, "bpf map list", nodeName) + output, err := runCiliumDbgCommand(ctx, "map list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list BPF maps: %v", err)), nil } @@ -1055,7 +1057,7 @@ func handleListMetrics(ctx context.Context, request mcp.CallToolRequest) (*mcp.C func handleListClusterNodes(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { nodeName := mcp.ParseString(request, "node_name", "") - output, err := runCiliumDbgCommand(ctx, "nodes list", nodeName) + output, err := runCiliumDbgCommand(ctx, "node list", nodeName) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to list cluster nodes: %v", err)), nil } diff --git a/pkg/cilium/cilium_test.go b/pkg/cilium/cilium_test.go index 5e4ec243..50bbed6d 100644 --- a/pkg/cilium/cilium_test.go +++ b/pkg/cilium/cilium_test.go @@ -258,6 +258,358 @@ func TestRunCiliumCliWithContext(t *testing.T) { }) } +// mockCiliumDbgCommand sets up the mock for a cilium-dbg command executed via kubectl exec. +// It mocks: (1) kubectl get pods to resolve the cilium pod name, (2) kubectl exec to run cilium-dbg. +func mockCiliumDbgCommand(mock *cmd.MockShellExecutor, dbgArgs []string, output string, err error) { + // Mock the pod name lookup + mock.AddCommandString("kubectl", []string{ + "get", "pods", "-n", "kube-system", + "--selector=k8s-app=cilium", + "--field-selector=spec.nodeName=test-node", + "-o", "jsonpath={.items[0].metadata.name}", + }, "cilium-abc123", nil) + + // Mock the kubectl exec call + execArgs := []string{"exec", "-n", "kube-system", "cilium-abc123", "--", "cilium-dbg"} + execArgs = append(execArgs, dbgArgs...) + mock.AddCommandString("kubectl", execArgs, output, err) +} + +func newRequestWithArgs(args map[string]any) mcp.CallToolRequest { + return mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Arguments: args, + }, + } +} + +func TestHandleGetEndpointsList(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"endpoint", "list"}, "ENDPOINT POLICY\n34 Disabled", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleGetEndpointsList(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "ENDPOINT") +} + +func TestHandleGetEndpointDetails(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"endpoint", "get", "34", "-o", "json"}, `{"id": 34}`, nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"endpoint_id": "34", "node_name": "test-node"}) + result, err := handleGetEndpointDetails(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), `"id": 34`) +} + +func TestHandleGetEndpointLogs(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"endpoint", "logs", "34"}, "endpoint log output", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"endpoint_id": "34", "node_name": "test-node"}) + result, err := handleGetEndpointLogs(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "endpoint log output") +} + +func TestHandleGetEndpointHealth(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"endpoint", "health", "34"}, "endpoint health OK", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"endpoint_id": "34", "node_name": "test-node"}) + result, err := handleGetEndpointHealth(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "endpoint health OK") +} + +func TestHandleShowConfigurationOptions(t *testing.T) { + t.Run("default", func(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"config"}, "PolicyEnforcement=default", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleShowConfigurationOptions(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "PolicyEnforcement") + }) + + t.Run("all", func(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"config", "--all"}, "all config options", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node", "list_all": "true"}) + result, err := handleShowConfigurationOptions(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "all config options") + }) + + t.Run("read_only", func(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"config", "-r"}, "read only config", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node", "list_read_only": "true"}) + result, err := handleShowConfigurationOptions(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "read only config") + }) +} + +func TestHandleToggleConfigurationOption(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"config", "PolicyEnforcement=enable"}, "option toggled", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"option": "PolicyEnforcement", "value": "true", "node_name": "test-node"}) + result, err := handleToggleConfigurationOption(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "option toggled") +} + +func TestHandleListIdentities(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"identity", "list"}, "ID LABELS\n1 reserved:host", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleListIdentities(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "reserved:host") +} + +func TestHandleGetDaemonStatus(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"status"}, "KVStore: Ok\nKubernetes: Ok", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleGetDaemonStatus(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "KVStore: Ok") +} + +func TestHandleDisplayEncryptionState(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"encrypt", "status"}, "Encryption: Disabled", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleDisplayEncryptionState(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "Encryption: Disabled") +} + +func TestHandleShowDNSNames(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"fqdn", "names"}, "DNS names output", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleShowDNSNames(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "DNS names output") +} + +func TestHandleFQDNCache(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"fqdn", "cache", "list"}, "FQDN cache entries", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleFQDNCache(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "FQDN cache entries") +} + +func TestHandleListClusterNodes(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"node", "list"}, "Name IPv4 Address\nnode1 10.0.0.1", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleListClusterNodes(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "node1") +} + +func TestHandleListNodeIds(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"nodeid", "list"}, "ID IP\n1 10.0.0.1", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleListNodeIds(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "10.0.0.1") +} + +func TestHandleListBPFMaps(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"map", "list"}, "Name Num entries\ncilium_lb4 22", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleListBPFMaps(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "cilium_lb4") +} + +func TestHandleGetBPFMap(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"map", "get", "cilium_lb4"}, "map contents", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"map_name": "cilium_lb4", "node_name": "test-node"}) + result, err := handleGetBPFMap(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "map contents") +} + +func TestHandleListBPFMapEvents(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"map", "events", "cilium_lb4"}, "map events", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"map_name": "cilium_lb4", "node_name": "test-node"}) + result, err := handleListBPFMapEvents(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "map events") +} + +func TestHandleListMetrics(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"metrics", "list"}, "Metric Value\ncilium_endpoint_count 4", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleListMetrics(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "cilium_endpoint_count") +} + +func TestHandleListServices(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"service", "list"}, "ID Frontend\n1 10.96.0.1:443", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleListServices(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "10.96.0.1") +} + +func TestHandleListIPAddresses(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"ip", "list"}, "IP Identity\n10.0.0.1 1", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleListIPAddresses(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "10.0.0.1") +} + +func TestHandleDisplaySelectors(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"policy", "selectors"}, "SELECTOR IDENTITIES", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleDisplaySelectors(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "SELECTOR") +} + +func TestHandleListLocalRedirectPolicies(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"lrp", "list"}, "No local redirect policies", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleListLocalRedirectPolicies(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "No local redirect policies") +} + +func TestHandleRequestDebuggingInformation(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"debuginfo"}, "debug info output", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleRequestDebuggingInformation(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "debug info output") +} + +func TestHandleListXDPCIDRFilters(t *testing.T) { + ctx := context.Background() + mock := cmd.NewMockShellExecutor() + mockCiliumDbgCommand(mock, []string{"prefilter", "list"}, "CIDR filters", nil) + ctx = cmd.WithShellExecutor(ctx, mock) + + req := newRequestWithArgs(map[string]any{"node_name": "test-node"}) + result, err := handleListXDPCIDRFilters(ctx, req) + require.NoError(t, err) + assert.False(t, result.IsError) + assert.Contains(t, getResultText(result), "CIDR filters") +} + func getResultText(r *mcp.CallToolResult) string { if r == nil || len(r.Content) == 0 { return "" diff --git a/scripts/cilium/install-cilium.sh b/scripts/cilium/install-cilium.sh new file mode 100755 index 00000000..922a67ff --- /dev/null +++ b/scripts/cilium/install-cilium.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Install Cilium into a Kind cluster via the kagent-tools pod. +# +# Prerequisites: +# - Kind cluster running with kagent-tools deployed +# +# Usage: +# ./scripts/cilium/install-cilium.sh [NAMESPACE] [RELEASE_NAME] [KUBE_CONTEXT] +# +# Defaults: +# NAMESPACE = kagent +# RELEASE_NAME = kagent-tools +# KUBE_CONTEXT = kind-kagent + +set -euo pipefail + +NAMESPACE="${1:-kagent}" +RELEASE_NAME="${2:-kagent-tools}" +KUBE_CONTEXT="${3:-kind-kagent}" + +echo "Installing Cilium via kagent-tools pod in namespace=$NAMESPACE context=$KUBE_CONTEXT" + +# Find the kagent-tools pod +POD=$(kubectl --context "$KUBE_CONTEXT" get pods -n "$NAMESPACE" \ + -l "app.kubernetes.io/instance=$RELEASE_NAME" \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + +if [ -z "$POD" ]; then + echo "ERROR: No kagent-tools pod found in namespace $NAMESPACE" + exit 1 +fi + +echo "Using pod: $POD" + +# Install Cilium +kubectl --context "$KUBE_CONTEXT" exec -n "$NAMESPACE" "$POD" -- \ + cilium install \ + --set routingMode=native \ + --set ipv4NativeRoutingCIDR=10.244.0.0/16 \ + --set bpf.masquerade=false + +echo "" +echo "Waiting for Cilium pods to be ready..." +kubectl --context "$KUBE_CONTEXT" wait \ + --for=condition=ready pod \ + -l k8s-app=cilium \ + -n kube-system \ + --timeout=120s + +echo "" +echo "Cilium status:" +kubectl --context "$KUBE_CONTEXT" exec -n "$NAMESPACE" "$POD" -- cilium status diff --git a/scripts/cilium/test-mcp-tools.sh b/scripts/cilium/test-mcp-tools.sh new file mode 100755 index 00000000..3ae07d9a --- /dev/null +++ b/scripts/cilium/test-mcp-tools.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Test Cilium MCP tools against a running kagent-tools instance. +# +# Prerequisites: +# - Kind cluster "kagent" running with Cilium installed +# - kagent-tools deployed and accessible on NodePort 30884 +# +# Usage: +# ./scripts/cilium/test-mcp-tools.sh [MCP_URL] [NODE_NAME] +# +# Defaults: +# MCP_URL = http://127.0.0.1:30884/mcp +# NODE_NAME = kagent-control-plane + +set -euo pipefail + +MCP_URL="${1:-http://127.0.0.1:30884/mcp}" +NODE_NAME="${2:-kagent-control-plane}" + +PASS=0 +FAIL=0 +SKIP=0 + +# Initialize MCP session +SESSION_ID=$(curl -sf -D - -X POST "$MCP_URL" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"cilium-test","version":"1.0"}}}' \ + 2>&1 | grep -i 'mcp-session-id' | tr -d '\r' | awk '{print $2}') + +if [ -z "$SESSION_ID" ]; then + echo "ERROR: Failed to initialize MCP session at $MCP_URL" + exit 1 +fi +echo "MCP session: $SESSION_ID" +echo "" + +ID=2 + +call_tool() { + local name=$1 + local args=$2 + + RESULT=$(curl -sf -X POST "$MCP_URL" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -H "Mcp-Session-Id: $SESSION_ID" \ + -d "{\"jsonrpc\":\"2.0\",\"id\":$ID,\"method\":\"tools/call\",\"params\":{\"name\":\"$name\",\"arguments\":$args}}" 2>&1 || true) + ID=$((ID + 1)) + + if [ -z "$RESULT" ]; then + echo "SKIP: $name (no response)" + SKIP=$((SKIP + 1)) + elif echo "$RESULT" | grep -q '"isError":true'; then + echo "FAIL: $name" + FAIL=$((FAIL + 1)) + else + echo "OK: $name" + PASS=$((PASS + 1)) + fi +} + +NODE_ARGS="{\"node_name\":\"$NODE_NAME\"}" + +echo "=== Cilium CLI tools ===" +call_tool "cilium_status_and_version" "{}" + +echo "" +echo "=== Cilium-dbg read-only tools ===" +call_tool "cilium_get_endpoints_list" "$NODE_ARGS" +call_tool "cilium_get_daemon_status" "$NODE_ARGS" +call_tool "cilium_list_identities" "$NODE_ARGS" +call_tool "cilium_display_encryption_state" "$NODE_ARGS" +call_tool "cilium_list_services" "$NODE_ARGS" +call_tool "cilium_list_metrics" "$NODE_ARGS" +call_tool "cilium_fqdn_cache" "$NODE_ARGS" +call_tool "cilium_show_dns_names" "$NODE_ARGS" +call_tool "cilium_show_configuration_options" "$NODE_ARGS" +call_tool "cilium_list_cluster_nodes" "$NODE_ARGS" +call_tool "cilium_list_node_ids" "$NODE_ARGS" +call_tool "cilium_list_bpf_maps" "$NODE_ARGS" +call_tool "cilium_list_ip_addresses" "$NODE_ARGS" +call_tool "cilium_display_selectors" "$NODE_ARGS" +call_tool "cilium_list_local_redirect_policies" "$NODE_ARGS" +call_tool "cilium_request_debugging_information" "$NODE_ARGS" +call_tool "cilium_show_load_information" "$NODE_ARGS" +call_tool "cilium_display_policy_node_information" "{\"node_name\":\"$NODE_NAME\",\"labels\":\"\"}" +call_tool "cilium_list_xdp_cidr_filters" "$NODE_ARGS" + +echo "" +echo "=== Results ===" +echo "PASS: $PASS FAIL: $FAIL SKIP: $SKIP TOTAL: $((PASS + FAIL + SKIP))" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi From 9274cb1dc54d29806d34e826a4e1b3197100d6f4 Mon Sep 17 00:00:00 2001 From: Jet Chiang Date: Tue, 5 May 2026 16:09:55 -0400 Subject: [PATCH 40/41] refactor(helm): remove rbac.clusterScoped, derive RBAC scope from rbac.namespaces (#57) * namespaced rbac update with kagent Signed-off-by: Jet Chiang * use proper helath check Signed-off-by: Jet Chiang --------- Signed-off-by: Jet Chiang --- helm/kagent-tools/templates/_helpers.tpl | 27 +++++- helm/kagent-tools/templates/clusterrole.yaml | 36 ++++---- .../templates/clusterrolebinding.yaml | 54 ++++++------ helm/kagent-tools/templates/deployment.yaml | 3 +- helm/kagent-tools/tests/rbac_test.yaml | 85 +++++++++++++++++-- helm/kagent-tools/values.yaml | 8 +- 6 files changed, 149 insertions(+), 64 deletions(-) diff --git a/helm/kagent-tools/templates/_helpers.tpl b/helm/kagent-tools/templates/_helpers.tpl index 40158ae0..f2d27f37 100644 --- a/helm/kagent-tools/templates/_helpers.tpl +++ b/helm/kagent-tools/templates/_helpers.tpl @@ -73,10 +73,29 @@ Service account name: default when useDefaultServiceAccount is true, otherwise t {{- end }} {{/* -Watch namespaces - transforms list of namespaces cached by the controller into comma-separated string -Removes duplicates +Watch namespaces - comma-separated list for controllers that watch a subset of namespaces. +Precedence: controller.watchNamespaces (explicit override) > rbac.namespaces > empty (watch all). */}} {{- define "kagent-tools.watchNamespaces" -}} -{{- $nsSet := dict }} -{{- .Values.controller.watchNamespaces | default list | uniq | join "," }} +{{- $ctrl := index .Values "controller" | default dict -}} +{{- if index $ctrl "watchNamespaces" -}} +{{- index $ctrl "watchNamespaces" | uniq | join "," -}} +{{- else if and .Values.rbac .Values.rbac.namespaces -}} +{{- .Values.rbac.namespaces | uniq | join "," -}} +{{- end -}} +{{- end }} + +{{/* +Guards on the rbac block +*/}} +{{- define "kagent-tools.rbac.validate" -}} +{{- if and .Values.rbac (hasKey .Values.rbac "clusterScoped") -}} +{{- fail "rbac.clusterScoped has been removed. Leave rbac.namespaces empty for cluster-scoped RBAC, or set rbac.namespaces=[, ...] for namespaced RBAC." -}} +{{- end -}} +{{- if and .Values.rbac .Values.rbac.namespaces -}} +{{- $installNs := include "kagent-tools.namespace" . -}} +{{- if not (has $installNs .Values.rbac.namespaces) -}} +{{- fail (printf "rbac.namespaces is set but does not include the install namespace %q" $installNs) -}} +{{- end -}} +{{- end -}} {{- end -}} diff --git a/helm/kagent-tools/templates/clusterrole.yaml b/helm/kagent-tools/templates/clusterrole.yaml index 48b615b9..2ddb85d4 100644 --- a/helm/kagent-tools/templates/clusterrole.yaml +++ b/helm/kagent-tools/templates/clusterrole.yaml @@ -74,7 +74,7 @@ - apiGroups: ["*"] resources: ["*"] verbs: ["*"] -{{- if .Values.rbac.clusterScoped }} +{{- if not .Values.rbac.namespaces }} - nonResourceURLs: ["*"] verbs: ["*"] {{- end }} @@ -82,23 +82,9 @@ {{- end -}} {{- if and (not .Values.useDefaultServiceAccount) .Values.rbac.create }} -{{- if .Values.rbac.clusterScoped }} -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - {{- if .Values.rbac.readOnly }} - name: {{ include "kagent-tools.fullname" . }}-read-role - {{- else }} - name: {{ include "kagent-tools.fullname" . }}-cluster-admin-role - {{- end }} - labels: - {{- include "kagent-tools.labels" . | nindent 4 }} -rules: - {{- include "kagent-tools.rules" . | nindent 2 }} - -{{- else }} -{{- $namespaces := .Values.rbac.namespaces | default (list (include "kagent-tools.namespace" .)) }} -{{- range $namespace := $namespaces }} +{{- include "kagent-tools.rbac.validate" . -}} +{{- if .Values.rbac.namespaces }} +{{- range $namespace := (.Values.rbac.namespaces | uniq | sortAlpha) }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -114,5 +100,19 @@ metadata: rules: {{- include "kagent-tools.rules" $ | nindent 2 }} {{- end }} + +{{- else }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + {{- if .Values.rbac.readOnly }} + name: {{ include "kagent-tools.fullname" . }}-read-role + {{- else }} + name: {{ include "kagent-tools.fullname" . }}-cluster-admin-role + {{- end }} + labels: + {{- include "kagent-tools.labels" . | nindent 4 }} +rules: + {{- include "kagent-tools.rules" . | nindent 2 }} {{- end }} {{- end }} diff --git a/helm/kagent-tools/templates/clusterrolebinding.yaml b/helm/kagent-tools/templates/clusterrolebinding.yaml index d6671a22..43240435 100644 --- a/helm/kagent-tools/templates/clusterrolebinding.yaml +++ b/helm/kagent-tools/templates/clusterrolebinding.yaml @@ -1,32 +1,8 @@ {{- if and (not .Values.useDefaultServiceAccount) .Values.rbac.create }} +{{- include "kagent-tools.rbac.validate" . -}} -{{- if .Values.rbac.clusterScoped }} -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - {{- if .Values.rbac.readOnly }} - name: {{ include "kagent-tools.fullname" . }}-read-rolebinding - {{- else }} - name: {{ include "kagent-tools.fullname" . }}-cluster-admin-rolebinding - {{- end }} - labels: - {{- include "kagent-tools.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - {{- if .Values.rbac.readOnly }} - name: {{ include "kagent-tools.fullname" . }}-read-role - {{- else }} - name: {{ include "kagent-tools.fullname" . }}-cluster-admin-role - {{- end }} -subjects: -- kind: ServiceAccount - name: {{ include "kagent-tools.fullname" . }} - namespace: {{ include "kagent-tools.namespace" . }} - -{{- else }} -{{- $namespaces := .Values.rbac.namespaces | default (list (include "kagent-tools.namespace" .)) }} -{{- range $namespace := $namespaces }} +{{- if .Values.rbac.namespaces }} +{{- range $namespace := (.Values.rbac.namespaces | uniq | sortAlpha) }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding @@ -52,6 +28,30 @@ subjects: name: {{ include "kagent-tools.fullname" $ }} namespace: {{ include "kagent-tools.namespace" $ }} {{- end }} + +{{- else }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + {{- if .Values.rbac.readOnly }} + name: {{ include "kagent-tools.fullname" . }}-read-rolebinding + {{- else }} + name: {{ include "kagent-tools.fullname" . }}-cluster-admin-rolebinding + {{- end }} + labels: + {{- include "kagent-tools.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + {{- if .Values.rbac.readOnly }} + name: {{ include "kagent-tools.fullname" . }}-read-role + {{- else }} + name: {{ include "kagent-tools.fullname" . }}-cluster-admin-role + {{- end }} +subjects: +- kind: ServiceAccount + name: {{ include "kagent-tools.fullname" . }} + namespace: {{ include "kagent-tools.namespace" . }} {{- end }} {{- end }} diff --git a/helm/kagent-tools/templates/deployment.yaml b/helm/kagent-tools/templates/deployment.yaml index a78779cd..c4b4a354 100644 --- a/helm/kagent-tools/templates/deployment.yaml +++ b/helm/kagent-tools/templates/deployment.yaml @@ -106,7 +106,8 @@ spec: containerPort: {{ .Values.tools.metrics.port | default .Values.service.ports.tools.targetPort }} protocol: TCP readinessProbe: - tcpSocket: + httpGet: + path: /health port: http-tools initialDelaySeconds: 15 periodSeconds: 15 diff --git a/helm/kagent-tools/tests/rbac_test.yaml b/helm/kagent-tools/tests/rbac_test.yaml index 15d05ca1..41a991e3 100644 --- a/helm/kagent-tools/tests/rbac_test.yaml +++ b/helm/kagent-tools/tests/rbac_test.yaml @@ -12,9 +12,10 @@ tests: of: ClusterRoleBinding template: clusterrolebinding.yaml - - it: should render Roles when clusterScoped is false + - it: should render Roles when rbac.namespaces is set set: - rbac.clusterScoped: false + rbac.namespaces: + - NAMESPACE asserts: - isKind: of: Role @@ -31,36 +32,102 @@ tests: value: NAMESPACE template: clusterrolebinding.yaml - - it: should render multiple roles and bindings when namespaces are specified and clusterScoped is false + - it: should render a single role/binding in the listed namespace only (no release-ns fallback) + set: + rbac.namespaces: + - NAMESPACE + asserts: + - hasDocuments: + count: 1 + template: clusterrole.yaml + - equal: + path: metadata.namespace + value: NAMESPACE + template: clusterrole.yaml + - hasDocuments: + count: 1 + template: clusterrolebinding.yaml + - equal: + path: metadata.namespace + value: NAMESPACE + template: clusterrolebinding.yaml + + - it: should render multiple roles and bindings when namespaces are specified set: - rbac.clusterScoped: false rbac.namespaces: - ns1 + - NAMESPACE - ns2 asserts: - hasDocuments: - count: 2 + count: 3 template: clusterrole.yaml - equal: path: metadata.namespace - value: ns1 + value: NAMESPACE template: clusterrole.yaml documentIndex: 0 - equal: path: metadata.namespace - value: ns2 + value: ns1 template: clusterrole.yaml documentIndex: 1 + - equal: + path: metadata.namespace + value: ns2 + template: clusterrole.yaml + documentIndex: 2 - hasDocuments: - count: 2 + count: 3 template: clusterrolebinding.yaml - equal: path: metadata.namespace - value: ns1 + value: NAMESPACE template: clusterrolebinding.yaml documentIndex: 0 + - equal: + path: metadata.namespace + value: ns1 + template: clusterrolebinding.yaml + documentIndex: 1 - equal: path: metadata.namespace value: ns2 template: clusterrolebinding.yaml + documentIndex: 2 + + - it: should fail rendering if the removed rbac.clusterScoped field is set + set: + rbac.clusterScoped: false + template: clusterrolebinding.yaml + asserts: + - failedTemplate: + errorMessage: "rbac.clusterScoped has been removed. Leave rbac.namespaces empty for cluster-scoped RBAC, or set rbac.namespaces=[, ...] for namespaced RBAC." + + - it: should fail rendering if rbac.namespaces is set but does not include the install namespace + set: + rbac.namespaces: + - some-other-ns + template: clusterrolebinding.yaml + asserts: + - failedTemplate: + errorMessage: 'rbac.namespaces is set but does not include the install namespace "NAMESPACE"' + + - it: should accept a custom install namespace when listed in rbac.namespaces + set: + namespaceOverride: my-ns + rbac.namespaces: + - my-ns + - other-ns + template: clusterrole.yaml + asserts: + - hasDocuments: + count: 2 + - equal: + path: metadata.namespace + value: my-ns + documentIndex: 0 + - equal: + path: metadata.namespace + value: other-ns documentIndex: 1 diff --git a/helm/kagent-tools/values.yaml b/helm/kagent-tools/values.yaml index 2246cf93..b398a00c 100644 --- a/helm/kagent-tools/values.yaml +++ b/helm/kagent-tools/values.yaml @@ -104,11 +104,9 @@ rbac: # When false, no ClusterRole or ClusterRoleBinding are created. # The ServiceAccount is still created allowing you to attach your own roles externally. create: true - # -- If true, creates ClusterRole and ClusterRoleBinding resources. - # If false, creates Role and RoleBinding resources instead. - clusterScoped: true - # -- When clusterScoped is false, specify additional namespaces to create Roles and RoleBindings in. - # If empty, defaults to the release namespace. + # -- Namespaces in which to create Role and RoleBinding resources. + # If empty (default), the chart creates cluster-scoped ClusterRole and ClusterRoleBinding resources. + # If set, the chart creates a Role + RoleBinding per listed namespace (install namespace must be included). namespaces: [] # When true, deploys a read-only ClusterRole (get, list, watch) instead of cluster-admin. # Pairs well with the --read-only CLI flag which disables write operations at the application layer. From 2a72881589f460380bd983500ad6f415e209f97f Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Thu, 7 May 2026 23:26:27 +0200 Subject: [PATCH 41/41] feat(k8s): add k8s_wait_for_condition tool Wraps kubectl wait so agents can block on a resource condition in one call instead of polling with repeated kubectl get turns. Closes #56 Co-authored-by: alexis-brettes <133014848+alexis-brettes@users.noreply.github.com> Signed-off-by: mesutoezdil --- pkg/k8s/k8s.go | 31 +++++++++++++++++++++ pkg/k8s/k8s_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index f84a0471..9a562f3d 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -571,6 +571,28 @@ func (k *K8sTool) handleGenerateResource(ctx context.Context, request mcp.CallTo return mcp.NewToolResultText(responseText), nil } +func (k *K8sTool) handleWaitForCondition(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + resourceType := mcp.ParseString(request, "resource_type", "") + resourceName := mcp.ParseString(request, "resource_name", "") + condition := mcp.ParseString(request, "condition", "") + namespace := mcp.ParseString(request, "namespace", "default") + timeoutSeconds := mcp.ParseInt(request, "timeout_seconds", 60) + + if resourceType == "" || resourceName == "" || condition == "" { + return mcp.NewToolResultError("resource_type, resource_name, and condition are required"), nil + } + if timeoutSeconds <= 0 { + return mcp.NewToolResultError("timeout_seconds must be greater than zero"), nil + } + + return k.runKubectlCommand(ctx, request.Header, + "wait", resourceType+"/"+resourceName, + "--for=condition="+condition, + "-n", namespace, + "--timeout="+fmt.Sprintf("%ds", timeoutSeconds), + ) +} + // extractBearerToken extracts the Bearer token from the Authorization header func extractBearerToken(headers http.Header) string { if auth := headers.Get("Authorization"); auth != "" { @@ -707,6 +729,15 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string, readO mcp.WithString("resource_type", mcp.Description(fmt.Sprintf("Type of resource to generate (%s)", strings.Join(slices.Collect(resourceTypes), ", "))), mcp.Required()), ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_generate_resource", k8sTool.handleGenerateResource))) + s.AddTool(mcp.NewTool("k8s_wait_for_condition", + mcp.WithDescription("Wait until a Kubernetes resource reaches a specific condition. Uses kubectl wait under the hood and blocks until the condition is met or the timeout expires. Avoids polling loops and saves LLM turns."), + mcp.WithString("resource_type", mcp.Description("Type of resource (deployment, pod, job, etc.)"), mcp.Required()), + mcp.WithString("resource_name", mcp.Description("Name of the resource"), mcp.Required()), + mcp.WithString("condition", mcp.Description("Condition to wait for (Available, Ready, Complete, etc.)"), mcp.Required()), + mcp.WithString("namespace", mcp.Description("Namespace of the resource (default: default)")), + mcp.WithNumber("timeout_seconds", mcp.Description("Maximum time to wait in seconds (default: 60)")), + ), telemetry.AdaptToolHandler(telemetry.WithTracing("k8s_wait_for_condition", k8sTool.handleWaitForCondition))) + // Write tools - only registered when write operations are enabled if !readOnly { s.AddTool(mcp.NewTool("k8s_scale", diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go index 44df8a92..0b35c1c1 100644 --- a/pkg/k8s/k8s_test.go +++ b/pkg/k8s/k8s_test.go @@ -1585,3 +1585,71 @@ metadata: assert.NotContains(t, callLog[0].Args, "--token") }) } + +func TestHandleWaitForCondition(t *testing.T) { + k8sTool := newTestK8sTool() + + tests := []struct { + name string + args map[string]interface{} + mock []string + mockErr error + wantErr bool + }{ + { + name: "success with defaults", + args: map[string]interface{}{"resource_type": "deployment", "resource_name": "myapp", "condition": "Available"}, + mock: []string{"wait", "deployment/myapp", "--for=condition=Available", "-n", "default", "--timeout=60s"}, + }, + { + name: "success with explicit namespace and timeout", + args: map[string]interface{}{"resource_type": "pod", "resource_name": "mypod", "condition": "Ready", "namespace": "kube-system", "timeout_seconds": float64(120)}, + mock: []string{"wait", "pod/mypod", "--for=condition=Ready", "-n", "kube-system", "--timeout=120s"}, + }, + { + name: "missing resource_type", + args: map[string]interface{}{"resource_name": "myapp", "condition": "Available"}, + wantErr: true, + }, + { + name: "missing resource_name", + args: map[string]interface{}{"resource_type": "deployment", "condition": "Available"}, + wantErr: true, + }, + { + name: "missing condition", + args: map[string]interface{}{"resource_type": "deployment", "resource_name": "myapp"}, + wantErr: true, + }, + { + name: "zero timeout", + args: map[string]interface{}{"resource_type": "deployment", "resource_name": "myapp", "condition": "Available", "timeout_seconds": float64(0)}, + wantErr: true, + }, + { + name: "kubectl error propagated", + args: map[string]interface{}{"resource_type": "deployment", "resource_name": "slow-app", "condition": "Available", "timeout_seconds": float64(5)}, + mock: []string{"wait", "deployment/slow-app", "--for=condition=Available", "-n", "default", "--timeout=5s"}, + mockErr: assert.AnError, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + if tt.mock != nil { + mock.AddCommandString("kubectl", tt.mock, "", tt.mockErr) + } + ctx := cmd.WithShellExecutor(context.Background(), mock) + + req := mcp.CallToolRequest{} + req.Params.Arguments = tt.args + + result, err := k8sTool.handleWaitForCondition(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tt.wantErr, result.IsError) + }) + } +}