mirror of
https://github.com/harivansh-afk/agentikube.git
synced 2026-04-15 17:00:58 +00:00
Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af0a16382f | ||
| 351bf2892f | |||
| 081739b9a3 | |||
| 165ac465e7 | |||
| 305c77eec9 | |||
| 44fe1e1f5c | |||
|
|
f3abdfe7b8 | ||
| 9b7c1c0054 |
19 changed files with 662 additions and 53 deletions
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
|
@ -20,8 +20,22 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Setup Helm
|
||||||
|
uses: azure/setup-helm@v4
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build ./...
|
run: go build ./...
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go 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
|
||||||
|
|
|
||||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
|
|
@ -9,6 +9,7 @@ on:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref }}
|
group: release-${{ github.ref }}
|
||||||
|
|
@ -29,6 +30,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Setup Helm
|
||||||
|
uses: azure/setup-helm@v4
|
||||||
|
|
||||||
- name: Compute next version
|
- name: Compute next version
|
||||||
id: version
|
id: version
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
@ -93,3 +97,17 @@ jobs:
|
||||||
name: ${{ steps.version.outputs.next_tag }}
|
name: ${{ steps.version.outputs.next_tag }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: dist/*
|
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 }}
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1 @@
|
||||||
.cache
|
.cache
|
||||||
agentikube
|
|
||||||
|
|
|
||||||
15
Makefile
15
Makefile
|
|
@ -1,4 +1,4 @@
|
||||||
.PHONY: build install clean fmt vet lint
|
.PHONY: build install clean fmt vet lint crds helm-lint helm-template
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -o agentikube ./cmd/agentikube
|
go build -o agentikube ./cmd/agentikube
|
||||||
|
|
@ -16,3 +16,16 @@ vet:
|
||||||
go vet ./...
|
go vet ./...
|
||||||
|
|
||||||
lint: fmt 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
|
||||||
|
|
|
||||||
103
README.md
103
README.md
|
|
@ -1,84 +1,85 @@
|
||||||
# agentikube
|
# agentikube
|
||||||
|
|
||||||
[](https://github.com/harivansh-afk/agentikube/blob/main/go.mod)
|
[](https://github.com/harivansh-afk/agentikube/blob/main/go.mod)
|
||||||
|
[](https://github.com/harivansh-afk/agentikube/tree/main/chart/agentikube)
|
||||||
[](https://github.com/harivansh-afk/agentikube/releases/latest)
|
[](https://github.com/harivansh-afk/agentikube/releases/latest)
|
||||||
|
|
||||||
A small Go CLI that spins up isolated agent sandboxes on Kubernetes.
|
Isolated stateful agent sandboxes on Kubernetes
|
||||||
|
|
||||||
<img width="1023" height="745" alt="image" src="https://github.com/user-attachments/assets/d62b6d99-b6bf-4ac3-9fb3-9b8373afbbec" />
|
<img width="1023" height="745" alt="image" src="https://github.com/user-attachments/assets/d62b6d99-b6bf-4ac3-9fb3-9b8373afbbec" />
|
||||||
|
|
||||||
## Context
|
## Install
|
||||||
|
|
||||||
https://hari.tech/thoughts/isolated-long-running-agents-with-kubernetes
|
|
||||||
|
|
||||||
## 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 <handle>`** - 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 <handle>`** - Drops you into a sandbox pod shell
|
|
||||||
- **`destroy <handle>`** - Tears down a single sandbox
|
|
||||||
- **`down`** - Removes shared infra but keeps existing user sandboxes
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Copy and fill in your config
|
helm install agentikube oci://ghcr.io/harivansh-afk/agentikube \
|
||||||
cp agentikube.example.yaml agentikube.yaml
|
-n sandboxes --create-namespace \
|
||||||
# Edit: namespace, EFS filesystem ID, sandbox image, compute settings
|
-f my-values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
# 2. Set things up
|
Create a `my-values.yaml` with your cluster details:
|
||||||
agentikube init
|
|
||||||
agentikube up
|
|
||||||
|
|
||||||
# 3. Create a sandbox and jump in
|
```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
|
||||||
agentikube create demo --provider openai --api-key <key>
|
agentikube create demo --provider openai --api-key <key>
|
||||||
agentikube list
|
agentikube list
|
||||||
agentikube ssh demo
|
agentikube ssh demo
|
||||||
|
agentikube status
|
||||||
|
agentikube destroy demo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Build it with `go build ./cmd/agentikube` or `make build`.
|
||||||
|
|
||||||
## What gets created
|
## What gets created
|
||||||
|
|
||||||
Running `up` applies these to your cluster:
|
The Helm chart installs:
|
||||||
|
|
||||||
- Namespace, StorageClass (`efs-sandbox`), SandboxTemplate
|
- StorageClass (`efs-sandbox`) backed by your EFS filesystem
|
||||||
- Optionally: SandboxWarmPool, NodePool + EC2NodeClass (Karpenter)
|
- SandboxTemplate defining the pod spec
|
||||||
|
- NetworkPolicy for ingress/egress rules
|
||||||
|
- SandboxWarmPool (optional, enabled by default)
|
||||||
|
- Karpenter NodePool + EC2NodeClass (optional, when `compute.type: karpenter`)
|
||||||
|
|
||||||
Running `create <handle>` adds:
|
Each `agentikube create <handle>` then adds a Secret, SandboxClaim, and workspace PVC for that user.
|
||||||
|
|
||||||
- A Secret and SandboxClaim per user
|
|
||||||
- A workspace PVC backed by EFS
|
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
```
|
```
|
||||||
cmd/agentikube/main.go # entrypoint
|
cmd/agentikube/ CLI entrypoint
|
||||||
internal/config/ # config structs + validation
|
internal/ config, manifest rendering, kube helpers
|
||||||
internal/manifest/ # template rendering
|
chart/agentikube/ Helm chart
|
||||||
internal/manifest/templates/ # k8s YAML templates
|
scripts/ CRD download helper
|
||||||
internal/kube/ # kube client helpers
|
|
||||||
internal/commands/ # command implementations
|
|
||||||
agentikube.example.yaml # example config
|
|
||||||
Makefile # build/install/fmt/vet
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build and test locally
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build ./...
|
make build # compile CLI
|
||||||
go test ./...
|
make helm-lint # lint the chart
|
||||||
go run ./cmd/agentikube --help
|
make helm-template # dry-run render
|
||||||
|
go test ./... # run tests
|
||||||
# Smoke test manifest generation
|
|
||||||
./agentikube up --dry-run --config agentikube.example.yaml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Good to know
|
## Good to know
|
||||||
|
|
||||||
- `storage.type` is `efs` only for now
|
- Storage is EFS-only for now
|
||||||
- `kubectl` needs to be installed (used by `init` and `ssh`)
|
- `kubectl` must be installed (used by `init` and `ssh`)
|
||||||
- Fargate is validated in config but templates only cover the Karpenter path so far
|
- Fargate is validated in config but templates only cover Karpenter 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
|
||||||
- [k9s](https://k9scli.io/) is great for browsing sandbox resources (`brew install derailed/k9s/k9s`)
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
(https://harivan.sh/thoughts/isolated-long-running-agents-with-kubernetes)
|
||||||
|
|
|
||||||
BIN
agentikube
Executable file
BIN
agentikube
Executable file
Binary file not shown.
12
chart/agentikube/Chart.yaml
Normal file
12
chart/agentikube/Chart.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
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
|
||||||
0
chart/agentikube/crds/.gitkeep
Normal file
0
chart/agentikube/crds/.gitkeep
Normal file
25
chart/agentikube/templates/NOTES.txt
Normal file
25
chart/agentikube/templates/NOTES.txt
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
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 <handle> --provider <provider> --api-key <key>
|
||||||
|
|
||||||
|
To list sandboxes:
|
||||||
|
agentikube list
|
||||||
|
|
||||||
|
To SSH into a sandbox:
|
||||||
|
agentikube ssh <handle>
|
||||||
|
|
||||||
|
To destroy a sandbox:
|
||||||
|
agentikube destroy <handle>
|
||||||
42
chart/agentikube/templates/_helpers.tpl
Normal file
42
chart/agentikube/templates/_helpers.tpl
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{{/*
|
||||||
|
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 }}
|
||||||
18
chart/agentikube/templates/karpenter-ec2nodeclass.yaml
Normal file
18
chart/agentikube/templates/karpenter-ec2nodeclass.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{{- 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 }}
|
||||||
37
chart/agentikube/templates/karpenter-nodepool.yaml
Normal file
37
chart/agentikube/templates/karpenter-nodepool.yaml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{{- 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 }}
|
||||||
28
chart/agentikube/templates/networkpolicy.yaml
Normal file
28
chart/agentikube/templates/networkpolicy.yaml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
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 }}
|
||||||
57
chart/agentikube/templates/sandbox-template.yaml
Normal file
57
chart/agentikube/templates/sandbox-template.yaml
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
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"
|
||||||
16
chart/agentikube/templates/storageclass-efs.yaml
Normal file
16
chart/agentikube/templates/storageclass-efs.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
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
|
||||||
14
chart/agentikube/templates/warm-pool.yaml
Normal file
14
chart/agentikube/templates/warm-pool.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{{- 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 }}
|
||||||
66
chart/agentikube/values.yaml
Normal file
66
chart/agentikube/values.yaml
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# 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
|
||||||
223
chart/agentikube_test.go
Normal file
223
chart/agentikube_test.go
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
26
scripts/download-crds.sh
Executable file
26
scripts/download-crds.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
||||||
|
#!/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}"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue