diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 845a263..a857e6e 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 0e361a1..e307408 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 86f1282..c75a184 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 b43bc83..6b556e1 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 71328c5..764e10b 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 9fdbfe5..de3cec9 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 ee80a32..d966102 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 d0246ac..7b1b4c0 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 6b54259..8f1cd64 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") }