diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 904259d..d4df1c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,22 +20,8 @@ jobs: with: go-version-file: go.mod - - name: Setup Helm - uses: azure/setup-helm@v4 - - name: Build run: go build ./... - name: Test run: go test ./... - - - name: Helm lint - run: helm lint chart/agentikube/ - - - name: Helm template - run: | - helm template agentikube chart/agentikube/ \ - --namespace sandboxes \ - --set storage.filesystemId=fs-test \ - --set sandbox.image=test:latest \ - --set compute.clusterName=test-cluster diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b754e7b..bbf975f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,6 @@ on: permissions: contents: write - packages: write concurrency: group: release-${{ github.ref }} @@ -30,9 +29,6 @@ jobs: with: go-version-file: go.mod - - name: Setup Helm - uses: azure/setup-helm@v4 - - name: Compute next version id: version shell: bash @@ -97,17 +93,3 @@ jobs: name: ${{ steps.version.outputs.next_tag }} generate_release_notes: true files: dist/* - - - name: Set chart version - run: | - sed -i "s/^version:.*/version: ${{ steps.version.outputs.version }}/" chart/agentikube/Chart.yaml - sed -i "s/^appVersion:.*/appVersion: \"${{ steps.version.outputs.version }}\"/" chart/agentikube/Chart.yaml - - - name: Package Helm chart - run: helm package chart/agentikube/ --destination .helm-pkg - - - name: Log in to GHCR - run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin - - - name: Push Helm chart to GHCR - run: helm push .helm-pkg/agentikube-${{ steps.version.outputs.version }}.tgz oci://ghcr.io/${{ github.repository_owner }} diff --git a/.gitignore b/.gitignore index 16d3c4d..6198882 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .cache +agentikube diff --git a/Makefile b/Makefile index f68ed93..160de4a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build install clean fmt vet lint crds helm-lint helm-template +.PHONY: build install clean fmt vet lint build: go build -o agentikube ./cmd/agentikube @@ -16,16 +16,3 @@ vet: go vet ./... lint: fmt vet - -crds: - ./scripts/download-crds.sh - -helm-lint: - helm lint chart/agentikube/ - -helm-template: - helm template agentikube chart/agentikube/ \ - --namespace sandboxes \ - --set storage.filesystemId=fs-test \ - --set sandbox.image=test:latest \ - --set compute.clusterName=test-cluster diff --git a/README.md b/README.md index d566e76..296157a 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,80 @@ # agentikube [![Go Version](https://img.shields.io/github/go-mod/go-version/harivansh-afk/agentikube)](https://github.com/harivansh-afk/agentikube/blob/main/go.mod) -[![Helm Version](https://img.shields.io/badge/helm%20chart-0.1.0-blue)](https://github.com/harivansh-afk/agentikube/tree/main/chart/agentikube) [![Release](https://img.shields.io/github/v/release/harivansh-afk/agentikube)](https://github.com/harivansh-afk/agentikube/releases/latest) -Isolated stateful agent sandboxes on Kubernetes +A small Go CLI that spins up isolated agent sandboxes on Kubernetes. Built for AWS setups (EFS + optional Karpenter). -image +image -## Install +## What it does + +- **`init`** - Installs CRDs, checks prerequisites, ensures your namespace exists +- **`up`** - Renders and applies Kubernetes manifests from templates (`--dry-run` to preview) +- **`create `** - Spins up a sandbox for a user with provider credentials +- **`list`** - Shows all sandboxes with status, age, and pod name +- **`status`** - Warm pool numbers, sandbox count, Karpenter node count +- **`ssh `** - Drops you into a sandbox pod shell +- **`destroy `** - Tears down a single sandbox +- **`down`** - Removes shared infra but keeps existing user sandboxes + +## Quick start ```bash -helm install agentikube oci://ghcr.io/harivansh-afk/agentikube \ - -n sandboxes --create-namespace \ - -f my-values.yaml -``` +# 1. Copy and fill in your config +cp agentikube.example.yaml agentikube.yaml +# Edit: namespace, EFS filesystem ID, sandbox image, compute settings -Create a `my-values.yaml` with your cluster details: +# 2. Set things up +agentikube init +agentikube up -```yaml -compute: - clusterName: my-eks-cluster -storage: - filesystemId: fs-0123456789abcdef0 -sandbox: - image: my-registry/sandbox:latest -``` - -See [`values.yaml`](chart/agentikube/values.yaml) for all options. - -## CLI - -The Go CLI handles runtime operations that are inherently imperative: - -```bash +# 3. Create a sandbox and jump in agentikube create demo --provider openai --api-key agentikube list agentikube ssh demo -agentikube status -agentikube destroy demo ``` -Build it with `go build ./cmd/agentikube` or `make build`. - ## What gets created -The Helm chart installs: +Running `up` applies these to your cluster: -- StorageClass (`efs-sandbox`) backed by your EFS filesystem -- SandboxTemplate defining the pod spec -- NetworkPolicy for ingress/egress rules -- SandboxWarmPool (optional, enabled by default) -- Karpenter NodePool + EC2NodeClass (optional, when `compute.type: karpenter`) +- Namespace, StorageClass (`efs-sandbox`), SandboxTemplate +- Optionally: SandboxWarmPool, NodePool + EC2NodeClass (Karpenter) -Each `agentikube create ` then adds a Secret, SandboxClaim, and workspace PVC for that user. +Running `create ` adds: + +- A Secret and SandboxClaim per user +- A workspace PVC backed by EFS ## Project layout ``` -cmd/agentikube/ CLI entrypoint -internal/ config, manifest rendering, kube helpers -chart/agentikube/ Helm chart -scripts/ CRD download helper +cmd/agentikube/main.go # entrypoint +internal/config/ # config structs + validation +internal/manifest/ # template rendering +internal/manifest/templates/ # k8s YAML templates +internal/kube/ # kube client helpers +internal/commands/ # command implementations +agentikube.example.yaml # example config +Makefile # build/install/fmt/vet ``` -## Development +## Build and test locally ```bash -make build # compile CLI -make helm-lint # lint the chart -make helm-template # dry-run render -go test ./... # run tests +go build ./... +go test ./... +go run ./cmd/agentikube --help + +# Smoke test manifest generation +./agentikube up --dry-run --config agentikube.example.yaml ``` ## Good to know -- Storage is EFS-only for now -- `kubectl` must be installed (used by `init` and `ssh`) -- Fargate is validated in config but templates only cover Karpenter so far -- [k9s](https://k9scli.io/) is great for browsing sandbox resources - -## Context - -(https://harivan.sh/thoughts/isolated-long-running-agents-with-kubernetes) +- `storage.type` is `efs` only for now +- `kubectl` needs to be installed (used by `init` and `ssh`) +- Fargate is validated in config but templates only cover the Karpenter path so far +- No Go tests written yet - `go test` passes but reports no test files +- [k9s](https://k9scli.io/) is great for browsing sandbox resources (`brew install derailed/k9s/k9s`) diff --git a/agentikube b/agentikube deleted file mode 100755 index 83f4562..0000000 Binary files a/agentikube and /dev/null differ diff --git a/chart/agentikube/Chart.yaml b/chart/agentikube/Chart.yaml deleted file mode 100644 index 293a85d..0000000 --- a/chart/agentikube/Chart.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v2 -name: agentikube -description: Isolated agent sandboxes on Kubernetes -type: application -version: 0.1.0 -appVersion: "0.1.0" -keywords: - - sandbox - - agents - - kubernetes - - karpenter - - efs diff --git a/chart/agentikube/crds/.gitkeep b/chart/agentikube/crds/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/chart/agentikube/templates/NOTES.txt b/chart/agentikube/templates/NOTES.txt deleted file mode 100644 index 1fcd08a..0000000 --- a/chart/agentikube/templates/NOTES.txt +++ /dev/null @@ -1,25 +0,0 @@ -agentikube has been installed in namespace {{ .Release.Namespace }}. - -Resources created: - - StorageClass: efs-sandbox (EFS filesystem: {{ .Values.storage.filesystemId }}) - - SandboxTemplate: sandbox-template -{{- if .Values.sandbox.warmPool.enabled }} - - SandboxWarmPool: sandbox-warm-pool ({{ .Values.sandbox.warmPool.size }} replicas) -{{- end }} -{{- if eq .Values.compute.type "karpenter" }} - - NodePool: sandbox-pool - - EC2NodeClass: sandbox-nodes -{{- end }} - - NetworkPolicy: sandbox-network-policy - -To create a sandbox: - agentikube create --provider --api-key - -To list sandboxes: - agentikube list - -To SSH into a sandbox: - agentikube ssh - -To destroy a sandbox: - agentikube destroy diff --git a/chart/agentikube/templates/_helpers.tpl b/chart/agentikube/templates/_helpers.tpl deleted file mode 100644 index c5210b9..0000000 --- a/chart/agentikube/templates/_helpers.tpl +++ /dev/null @@ -1,42 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "agentikube.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -*/}} -{{- define "agentikube.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "agentikube.labels" -}} -helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{ include "agentikube.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "agentikube.selectorLabels" -}} -app.kubernetes.io/name: {{ include "agentikube.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} diff --git a/chart/agentikube/templates/karpenter-ec2nodeclass.yaml b/chart/agentikube/templates/karpenter-ec2nodeclass.yaml deleted file mode 100644 index b2f4959..0000000 --- a/chart/agentikube/templates/karpenter-ec2nodeclass.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if eq .Values.compute.type "karpenter" }} -apiVersion: karpenter.k8s.aws/v1 -kind: EC2NodeClass -metadata: - name: sandbox-nodes - labels: - {{- include "agentikube.labels" . | nindent 4 }} -spec: - amiSelectorTerms: - - alias: "al2023@latest" - subnetSelectorTerms: - - tags: - karpenter.sh/discovery: {{ required "compute.clusterName is required for Karpenter" .Values.compute.clusterName | quote }} - securityGroupSelectorTerms: - - tags: - karpenter.sh/discovery: {{ .Values.compute.clusterName | quote }} - role: {{ printf "KarpenterNodeRole-%s" .Values.compute.clusterName | quote }} -{{- end }} diff --git a/chart/agentikube/templates/karpenter-nodepool.yaml b/chart/agentikube/templates/karpenter-nodepool.yaml deleted file mode 100644 index 4d55cb4..0000000 --- a/chart/agentikube/templates/karpenter-nodepool.yaml +++ /dev/null @@ -1,37 +0,0 @@ -{{- if eq .Values.compute.type "karpenter" }} -apiVersion: karpenter.sh/v1 -kind: NodePool -metadata: - name: sandbox-pool - labels: - {{- include "agentikube.labels" . | nindent 4 }} -spec: - template: - spec: - requirements: - - key: node.kubernetes.io/instance-type - operator: In - values: - {{- range .Values.compute.instanceTypes }} - - {{ . }} - {{- end }} - - key: karpenter.sh/capacity-type - operator: In - values: - {{- range .Values.compute.capacityTypes }} - - {{ . }} - {{- end }} - - key: kubernetes.io/arch - operator: In - values: - - amd64 - nodeClassRef: - name: sandbox-nodes - group: karpenter.k8s.aws - kind: EC2NodeClass - limits: - cpu: {{ .Values.compute.maxCpu }} - memory: {{ .Values.compute.maxMemory }} - disruption: - consolidationPolicy: {{ if .Values.compute.consolidation }}WhenEmptyOrUnderutilized{{ else }}WhenEmpty{{ end }} -{{- end }} diff --git a/chart/agentikube/templates/networkpolicy.yaml b/chart/agentikube/templates/networkpolicy.yaml deleted file mode 100644 index fbad38e..0000000 --- a/chart/agentikube/templates/networkpolicy.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: sandbox-network-policy - namespace: {{ .Release.Namespace }} - labels: - {{- include "agentikube.labels" . | nindent 4 }} -spec: - podSelector: - matchLabels: - app.kubernetes.io/name: sandbox - policyTypes: - - Ingress - {{- if .Values.sandbox.networkPolicy.egressAllowAll }} - - Egress - {{- end }} - {{- if .Values.sandbox.networkPolicy.egressAllowAll }} - egress: - - to: - - ipBlock: - cidr: 0.0.0.0/0 - {{- end }} - ingress: - {{- range .Values.sandbox.networkPolicy.ingressPorts }} - - ports: - - port: {{ . }} - protocol: TCP - {{- end }} diff --git a/chart/agentikube/templates/sandbox-template.yaml b/chart/agentikube/templates/sandbox-template.yaml deleted file mode 100644 index 2c61361..0000000 --- a/chart/agentikube/templates/sandbox-template.yaml +++ /dev/null @@ -1,57 +0,0 @@ -apiVersion: extensions.agents.x-k8s.io/v1alpha1 -kind: SandboxTemplate -metadata: - name: sandbox-template - namespace: {{ .Release.Namespace }} - labels: - {{- include "agentikube.labels" . | nindent 4 }} -spec: - template: - spec: - containers: - - name: sandbox - image: {{ required "sandbox.image is required" .Values.sandbox.image }} - ports: - {{- range .Values.sandbox.ports }} - - containerPort: {{ . }} - {{- end }} - resources: - requests: - cpu: {{ .Values.sandbox.resources.requests.cpu }} - memory: {{ .Values.sandbox.resources.requests.memory }} - limits: - cpu: {{ .Values.sandbox.resources.limits.cpu | quote }} - memory: {{ .Values.sandbox.resources.limits.memory }} - securityContext: - runAsUser: {{ .Values.sandbox.securityContext.runAsUser }} - runAsGroup: {{ .Values.sandbox.securityContext.runAsGroup }} - runAsNonRoot: {{ .Values.sandbox.securityContext.runAsNonRoot }} - {{- if .Values.sandbox.env }} - env: - {{- range $key, $value := .Values.sandbox.env }} - - name: {{ $key }} - value: {{ $value | quote }} - {{- end }} - {{- end }} - startupProbe: - tcpSocket: - port: {{ .Values.sandbox.probes.port }} - failureThreshold: {{ .Values.sandbox.probes.startupFailureThreshold }} - periodSeconds: 10 - readinessProbe: - tcpSocket: - port: {{ .Values.sandbox.probes.port }} - periodSeconds: 10 - volumeMounts: - - name: workspace - mountPath: {{ .Values.sandbox.mountPath }} - volumeClaimTemplates: - - metadata: - name: workspace - spec: - accessModes: - - ReadWriteMany - storageClassName: efs-sandbox - resources: - requests: - storage: "10Gi" diff --git a/chart/agentikube/templates/storageclass-efs.yaml b/chart/agentikube/templates/storageclass-efs.yaml deleted file mode 100644 index 8a9c2ff..0000000 --- a/chart/agentikube/templates/storageclass-efs.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: storage.k8s.io/v1 -kind: StorageClass -metadata: - name: efs-sandbox - labels: - {{- include "agentikube.labels" . | nindent 4 }} -provisioner: efs.csi.aws.com -parameters: - provisioningMode: efs-ap - fileSystemId: {{ required "storage.filesystemId is required" .Values.storage.filesystemId }} - directoryPerms: "755" - uid: {{ .Values.storage.uid | quote }} - gid: {{ .Values.storage.gid | quote }} - basePath: {{ .Values.storage.basePath }} -reclaimPolicy: {{ .Values.storage.reclaimPolicy }} -volumeBindingMode: Immediate diff --git a/chart/agentikube/templates/warm-pool.yaml b/chart/agentikube/templates/warm-pool.yaml deleted file mode 100644 index 52f726f..0000000 --- a/chart/agentikube/templates/warm-pool.yaml +++ /dev/null @@ -1,14 +0,0 @@ -{{- if .Values.sandbox.warmPool.enabled }} -apiVersion: extensions.agents.x-k8s.io/v1alpha1 -kind: SandboxWarmPool -metadata: - name: sandbox-warm-pool - namespace: {{ .Release.Namespace }} - labels: - {{- include "agentikube.labels" . | nindent 4 }} -spec: - templateRef: - name: sandbox-template - replicas: {{ .Values.sandbox.warmPool.size }} - ttlMinutes: {{ .Values.sandbox.warmPool.ttlMinutes }} -{{- end }} diff --git a/chart/agentikube/values.yaml b/chart/agentikube/values.yaml deleted file mode 100644 index f40ece8..0000000 --- a/chart/agentikube/values.yaml +++ /dev/null @@ -1,66 +0,0 @@ -# Compute configuration for sandbox nodes -compute: - # karpenter or fargate - type: karpenter - instanceTypes: - - m6i.xlarge - - m5.xlarge - - r6i.xlarge - capacityTypes: - - spot - - on-demand - maxCpu: 2000 - maxMemory: 8000Gi - consolidation: true - # EKS cluster name - used for Karpenter subnet/SG/role discovery - clusterName: "" - -# Persistent storage configuration -storage: - # efs is the only supported type - type: efs - # REQUIRED - your EFS filesystem ID - filesystemId: "" - basePath: /sandboxes - uid: 1000 - gid: 1000 - reclaimPolicy: Retain - -# Sandbox pod configuration -sandbox: - # REQUIRED - container image for sandbox pods - image: "" - ports: - - 18789 - - 2222 - - 3000 - - 5173 - - 8080 - mountPath: /home/node/.openclaw - resources: - requests: - cpu: 50m - memory: 512Mi - limits: - cpu: "2" - memory: 4Gi - env: {} - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - runAsNonRoot: true - probes: - port: 18789 - startupFailureThreshold: 30 - warmPool: - enabled: true - size: 5 - ttlMinutes: 120 - networkPolicy: - egressAllowAll: true - ingressPorts: - - 18789 - - 2222 - - 3000 - - 5173 - - 8080 diff --git a/chart/agentikube_test.go b/chart/agentikube_test.go deleted file mode 100644 index 9854572..0000000 --- a/chart/agentikube_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package chart_test - -import ( - "os" - "os/exec" - "strings" - "testing" -) - -// helmTemplate runs helm template with the given extra args and returns stdout. -func helmTemplate(t *testing.T, extraArgs ...string) string { - t.Helper() - args := []string{ - "template", "agentikube", "chart/agentikube/", - "--namespace", "sandboxes", - "--set", "storage.filesystemId=fs-test", - "--set", "sandbox.image=test:latest", - "--set", "compute.clusterName=test-cluster", - } - args = append(args, extraArgs...) - cmd := exec.Command("helm", args...) - cmd.Dir = repoRoot(t) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("helm template failed: %v\n%s", err, out) - } - return string(out) -} - -func repoRoot(t *testing.T) string { - t.Helper() - dir, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - // This test file lives at chart/agentikube_test.go, so repo root is .. - return dir + "/.." -} - -func TestHelmLint(t *testing.T) { - cmd := exec.Command("helm", "lint", "chart/agentikube/") - cmd.Dir = repoRoot(t) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("helm lint failed: %v\n%s", err, out) - } - if !strings.Contains(string(out), "0 chart(s) failed") { - t.Fatalf("helm lint reported failures:\n%s", out) - } -} - -func TestHelmTemplateDefaultValues(t *testing.T) { - output := helmTemplate(t) - - expected := []string{ - "kind: StorageClass", - "kind: SandboxTemplate", - "kind: SandboxWarmPool", - "kind: NodePool", - "kind: EC2NodeClass", - "kind: NetworkPolicy", - } - for _, want := range expected { - if !strings.Contains(output, want) { - t.Errorf("expected %q in rendered output", want) - } - } -} - -func TestHelmTemplateLabels(t *testing.T) { - output := helmTemplate(t) - - labels := []string{ - "helm.sh/chart: agentikube-0.1.0", - "app.kubernetes.io/name: agentikube", - "app.kubernetes.io/instance: agentikube", - "app.kubernetes.io/managed-by: Helm", - `app.kubernetes.io/version: "0.1.0"`, - } - for _, label := range labels { - if !strings.Contains(output, label) { - t.Errorf("expected label %q in rendered output", label) - } - } -} - -func TestHelmTemplateKarpenterDisabled(t *testing.T) { - output := helmTemplate(t, "--set", "compute.type=fargate") - - if strings.Contains(output, "kind: NodePool") { - t.Error("NodePool should not be rendered when compute.type=fargate") - } - if strings.Contains(output, "kind: EC2NodeClass") { - t.Error("EC2NodeClass should not be rendered when compute.type=fargate") - } - if !strings.Contains(output, "kind: StorageClass") { - t.Error("StorageClass should always be rendered") - } - if !strings.Contains(output, "kind: SandboxTemplate") { - t.Error("SandboxTemplate should always be rendered") - } -} - -func TestHelmTemplateWarmPoolDisabled(t *testing.T) { - output := helmTemplate(t, "--set", "sandbox.warmPool.enabled=false") - - if strings.Contains(output, "kind: SandboxWarmPool") { - t.Error("SandboxWarmPool should not be rendered when warmPool.enabled=false") - } - if !strings.Contains(output, "kind: SandboxTemplate") { - t.Error("SandboxTemplate should always be rendered") - } -} - -func TestHelmTemplateEgressDisabled(t *testing.T) { - output := helmTemplate(t, - "--set", "sandbox.networkPolicy.egressAllowAll=false", - "-s", "templates/networkpolicy.yaml", - ) - - if strings.Contains(output, "0.0.0.0/0") { - t.Error("egress CIDR should not appear when egressAllowAll=false") - } - lines := strings.Split(output, "\n") - for i, line := range lines { - if strings.Contains(line, "policyTypes:") { - block := strings.Join(lines[i:min(i+4, len(lines))], "\n") - if strings.Contains(block, "Egress") { - t.Error("Egress should not be in policyTypes when egressAllowAll=false") - } - } - } -} - -func TestHelmTemplateRequiredValues(t *testing.T) { - tests := []struct { - name string - args []string - wantErr string - }{ - { - name: "missing filesystemId", - args: []string{"--set", "sandbox.image=test:latest", "--set", "compute.clusterName=test"}, - wantErr: "storage.filesystemId is required", - }, - { - name: "missing sandbox image", - args: []string{"--set", "storage.filesystemId=fs-test", "--set", "compute.clusterName=test"}, - wantErr: "sandbox.image is required", - }, - { - name: "missing clusterName for karpenter", - args: []string{"--set", "storage.filesystemId=fs-test", "--set", "sandbox.image=test:latest"}, - wantErr: "compute.clusterName is required for Karpenter", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - args := append([]string{ - "template", "agentikube", "chart/agentikube/", - "--namespace", "sandboxes", - }, tt.args...) - cmd := exec.Command("helm", args...) - cmd.Dir = repoRoot(t) - out, err := cmd.CombinedOutput() - if err == nil { - t.Fatal("expected helm template to fail for missing required value") - } - if !strings.Contains(string(out), tt.wantErr) { - t.Errorf("expected error containing %q, got:\n%s", tt.wantErr, out) - } - }) - } -} - -func TestHelmTemplateEnvVars(t *testing.T) { - output := helmTemplate(t, - "--set", "sandbox.env.MY_VAR=my-value", - "-s", "templates/sandbox-template.yaml", - ) - - if !strings.Contains(output, "MY_VAR") { - t.Error("expected MY_VAR in rendered env") - } - if !strings.Contains(output, "my-value") { - t.Error("expected my-value in rendered env") - } -} - -func TestHelmTemplateNoEnvWhenEmpty(t *testing.T) { - output := helmTemplate(t, "-s", "templates/sandbox-template.yaml") - - lines := strings.Split(output, "\n") - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == "env:" { - t.Error("env: block should not appear when sandbox.env is empty") - } - } -} - -func TestHelmTemplateNamespace(t *testing.T) { - output := helmTemplate(t, "--namespace", "custom-ns") - - if !strings.Contains(output, "namespace: custom-ns") { - t.Error("expected namespace: custom-ns in rendered output") - } -} - -func TestHelmTemplateConsolidationDisabled(t *testing.T) { - output := helmTemplate(t, - "--set", "compute.consolidation=false", - "-s", "templates/karpenter-nodepool.yaml", - ) - - if !strings.Contains(output, "consolidationPolicy: WhenEmpty") { - t.Error("expected consolidationPolicy: WhenEmpty when consolidation=false") - } - if strings.Contains(output, "WhenEmptyOrUnderutilized") { - t.Error("should not have WhenEmptyOrUnderutilized when consolidation=false") - } -} diff --git a/scripts/download-crds.sh b/scripts/download-crds.sh deleted file mode 100755 index f4090be..0000000 --- a/scripts/download-crds.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Download agent-sandbox CRDs into chart/agentikube/crds/ -# Run this before packaging the chart: make crds - -REPO="kubernetes-sigs/agent-sandbox" -BRANCH="main" -BASE_URL="https://raw.githubusercontent.com/${REPO}/${BRANCH}/k8s/crds" -DEST="$(cd "$(dirname "$0")/.." && pwd)/chart/agentikube/crds" - -CRDS=( - sandboxtemplates.yaml - sandboxclaims.yaml - sandboxwarmpools.yaml -) - -echo "Downloading CRDs from ${REPO}@${BRANCH} ..." -mkdir -p "$DEST" - -for crd in "${CRDS[@]}"; do - echo " ${crd}" - curl -sSfL "${BASE_URL}/${crd}" -o "${DEST}/${crd}" -done - -echo "CRDs written to ${DEST}"