Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions kubernetes/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Kubernetes Operator

## Overview

Kubernetes operator managing sandbox environments via custom resources. Provides BatchSandbox (O(1) batch delivery), Pool (resource pooling for fast provisioning), and optional task orchestration. Built with controller-runtime (Kubebuilder).

## Structure

```
kubernetes/
├── apis/sandbox/v1alpha1/ # CRD type definitions
│ ├── batchsandbox_types.go # BatchSandbox spec + status
│ ├── pool_types.go # Pool spec + status
│ └── sandboxsnapshot_types.go
├── cmd/
│ ├── controller/main.go # Controller manager entry point
│ ├── image-committer/main.go # Image committer binary (runs as commit Job)
│ └── task-executor/main.go # Task executor binary (runs as sidecar)
├── internal/
│ ├── controller/ # Reconciliation loops
│ ├── scheduler/ # Pool allocation logic (bufferMin/Max, poolMax)
│ └── utils/ # Utility functions
├── config/
│ ├── crd/bases/ # Generated CRD YAML manifests
│ ├── rbac/ # ClusterRole, ClusterRoleBinding
│ ├── manager/ # Controller deployment manifest
│ └── samples/ # Example CRD instances
├── charts/ # Helm charts (opensandbox-controller, opensandbox-server, opensandbox)
├── test/e2e/ # End-to-end tests + testdata
└── Dockerfile # Controller image build
Dockerfile.image-committer # Image-committer image build
```

## Where to Look

| Task | File | Notes |
|------|------|-------|
| Add CRD field | `apis/sandbox/v1alpha1/*_types.go` | Run `make install` to update CRDs |
| Controller logic | `internal/controller/` | BatchSandbox + Pool reconciliation |
| Pool allocation | `internal/scheduler/` | Buffer management, sandbox→pool assignment |
| Task execution | `cmd/task-executor/`, `internal/task-executor/` | Process-based tasks in sandboxes |
| Helm values | `charts/opensandbox-controller/values.yaml` | Controller + task-executor image refs |
| RBAC permissions | `config/rbac/` | ClusterRole rules |
| E2E tests | `test/e2e/` | Ginkgo/Gomega test framework |

## Conventions

- **Framework**: Kubebuilder with `controller-runtime` v0.21.
- **Go version**: 1.24. Own `go.mod` (`github.com/alibaba/opensandbox/sandbox-k8s`).
- **Concurrency**: BatchSandbox controller concurrency=32, Pool controller concurrency=1.
- **CRD version**: `v1alpha1` under group `sandbox.opensandbox.io`.
- **Helm charts**: Umbrella chart (`opensandbox`) wraps controller + server subcharts.
- **Logging**: `klog/v2` + `zap`. Log level configurable via `--zap-log-level` flag.

## Anti-Patterns

- `pause`/`resume` lifecycle uses SandboxSnapshot CRD + image-committer Job to snapshot and restore containers.
- BatchSandbox deletion waits for running tasks to terminate before removing the resource.
- Task-executor requires `shareProcessNamespace: true` and `SYS_PTRACE` capability in pod spec.
- Pool template changes do not affect already-allocated sandboxes.

## Commands

```bash
make install # install CRDs into cluster
make deploy CONTROLLER_IMG=... TASK_EXECUTOR_IMG=... # deploy controller
make docker-build # build controller image
make docker-build-task-executor # build task-executor image
make docker-build-image-committer # build image-committer image
make test # run tests
```
56 changes: 56 additions & 0 deletions kubernetes/Dockerfile.image-committer
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2025 Alibaba Group Holding Ltd.
#
# 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.

# Build stage
FROM golang:1.24-alpine AS builder

# Use Aliyun mirror for faster downloads in China
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

WORKDIR /workspace

# Copy go mod files
COPY go.mod go.sum ./
RUN GOPROXY=https://goproxy.cn,direct go mod download

# Copy source code
COPY cmd/image-committer/ cmd/image-committer/

# Build binary
RUN CGO_ENABLED=0 GOOS=linux go build -o /usr/local/bin/image-committer ./cmd/image-committer/

# Runtime stage
FROM alpine:3.19

# Use Aliyun mirror for faster downloads in China
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

# Install containerd CLI tools
RUN apk add --no-cache \
containerd-ctr \
cri-tools \
curl \
jq \
nerdctl

# Create directories for socket mounts
RUN mkdir -p /var/run/containerd /run/k8s/containerd

# Copy the built binary from builder stage
COPY --from=builder /usr/local/bin/image-committer /usr/local/bin/image-committer
RUN chmod +x /usr/local/bin/image-committer

WORKDIR /workspace

ENTRYPOINT ["/usr/local/bin/image-committer"]
23 changes: 22 additions & 1 deletion kubernetes/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ OPERATOR_SDK_VERSION ?= v1.42.0
CONTROLLER_IMG ?= controller:dev
# TASK_EXECUTOR_IMG defines the image for the task-executor service.
TASK_EXECUTOR_IMG ?= task-executor:dev
# IMAGE_COMMITTER_IMG defines the image for the image-committer service.
IMAGE_COMMITTER_IMG ?= image-committer:dev

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
Expand Down Expand Up @@ -122,7 +124,7 @@ test: manifests generate fmt vet setup-envtest ## Run tests.
# To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
KIND_CLUSTER ?= sandbox-k8s-test-e2e
KIND_K8S_VERSION ?= v1.22.4
KIND_K8S_VERSION ?= v1.27.3
GINKGO_ARGS ?=

.PHONY: install-kind
Expand Down Expand Up @@ -165,6 +167,17 @@ test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expect
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
@$(KIND) delete cluster --name $(KIND_CLUSTER)

# Pause/Resume E2E test variables
REGISTRY_IMAGE ?= registry:2
REGISTRY_NODE_PORT ?= 30500
REGISTRY_USERNAME ?= testuser
REGISTRY_PASSWORD ?= testpass

.PHONY: test-e2e-pause-resume
test-e2e-pause-resume: setup-test-e2e ## Run pause/resume E2E tests
CONTROLLER_IMG=$(CONTROLLER_IMG) TASK_EXECUTOR_IMG=$(TASK_EXECUTOR_IMG) \
KIND_CLUSTER=$(KIND_CLUSTER) go test ./test/e2e/ -v -ginkgo.v -ginkgo.focus="PauseResume"

# Common E2E setup targets - install CRDs and controller for any Kind cluster
.PHONY: install-e2e-deps
install-e2e-deps:
Expand Down Expand Up @@ -278,6 +291,14 @@ docker-build-controller: ## Build docker image with the manager.
docker-build-task-executor: ## Build docker image with task-executor.
$(CONTAINER_TOOL) build $(DOCKER_BUILD_ARGS) --build-arg PACKAGE=cmd/task-executor/main.go --build-arg USERID=0 -t ${TASK_EXECUTOR_IMG} .

.PHONY: docker-build-image-committer
docker-build-image-committer: ## Build docker image for image commit operations.
$(CONTAINER_TOOL) build $(DOCKER_BUILD_ARGS) -f Dockerfile.image-committer -t ${IMAGE_COMMITTER_IMG} .

.PHONY: docker-push-image-committer
docker-push-image-committer: ## Push docker image for image-committer.
$(CONTAINER_TOOL) push ${IMAGE_COMMITTER_IMG}

.PHONY: docker-push
# docker-push: ## Push docker image with the manager.
# $(CONTAINER_TOOL) push ${CONTROLLER_IMG}
Expand Down
26 changes: 26 additions & 0 deletions kubernetes/apis/sandbox/v1alpha1/batchsandbox_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ type BatchSandboxSpec struct {
// +kubebuilder:default=Retain
// +kubebuilder:validation:Optional
TaskResourcePolicyWhenCompleted *TaskResourcePolicy `json:"taskResourcePolicyWhenCompleted,omitempty"`
// PausePolicy defines the pause/resume policy for this sandbox.
// +optional
PausePolicy *PausePolicy `json:"pausePolicy,omitempty"`
}

type TaskResourcePolicy string
Expand All @@ -79,6 +82,29 @@ const (
TaskResourcePolicyRelease TaskResourcePolicy = "Release"
)

// PausePolicy defines the policy for pause/resume operations.
type PausePolicy struct {
// SnapshotRegistry is the OCI registry for snapshot images.
// +kubebuilder:validation:Required
SnapshotRegistry string `json:"snapshotRegistry"`

// SnapshotType indicates the type of snapshot (default: Rootfs).
// +optional
// +kubebuilder:validation:Optional
// +kubebuilder:default=Rootfs
SnapshotType SnapshotType `json:"snapshotType,omitempty"`

// SnapshotPushSecret is the Secret name for pushing snapshots.
// +optional
// +kubebuilder:validation:Optional
SnapshotPushSecret string `json:"snapshotPushSecret,omitempty"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Align PausePolicy secret JSON keys with the public API

The PausePolicy struct uses snapshotPushSecret/resumeImagePullSecret JSON keys, but the server request schema and generated CRD use snapshotPushSecretName/resumeImagePullSecretName. This mismatch causes secret fields to be dropped when decoding BatchSandbox into typed Go structs, so pause/resume flows lose registry credentials (commit jobs cannot push to private registries and resumed sandboxes can miss image pull secrets).

Useful? React with 👍 / 👎.


// ResumeImagePullSecret is the Secret name for pulling snapshots during resume.
// +optional
// +kubebuilder:validation:Optional
ResumeImagePullSecret string `json:"resumeImagePullSecret,omitempty"`
}

// BatchSandboxStatus defines the observed state of BatchSandbox.
type BatchSandboxStatus struct {
// ObservedGeneration is the most recent generation observed for this BatchSandbox. It corresponds to the
Expand Down
175 changes: 175 additions & 0 deletions kubernetes/apis/sandbox/v1alpha1/sandboxsnapshot_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright 2025 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)

// SnapshotType defines the type of snapshot.
type SnapshotType string

const (
SnapshotTypeRootfs SnapshotType = "Rootfs"
)

// +kubebuilder:validation:Enum=Pending;Committing;Ready;Failed
// SandboxSnapshotPhase defines the phase of a snapshot.
type SandboxSnapshotPhase string

const (
SandboxSnapshotPhasePending SandboxSnapshotPhase = "Pending"
SandboxSnapshotPhaseCommitting SandboxSnapshotPhase = "Committing"
SandboxSnapshotPhaseReady SandboxSnapshotPhase = "Ready"
SandboxSnapshotPhaseFailed SandboxSnapshotPhase = "Failed"
)

// ContainerSnapshot represents a snapshot of a single container.
type ContainerSnapshot struct {
// ContainerName is the name of the container.
ContainerName string `json:"containerName"`
// ImageURI is the target image URI for this container's snapshot.
ImageURI string `json:"imageUri"`
// ImageDigest is the digest of the pushed snapshot image.
// +optional
ImageDigest string `json:"imageDigest,omitempty"`
}

// SandboxSnapshotSpec defines the desired state of SandboxSnapshot.
type SandboxSnapshotSpec struct {
// SandboxID is the stable public identifier for the sandbox.
SandboxID string `json:"sandboxId"`

// SnapshotType indicates the type of snapshot (default: Rootfs).
// +optional
// +kubebuilder:validation:Optional
// +kubebuilder:default=Rootfs
SnapshotType SnapshotType `json:"snapshotType,omitempty"`

// SourceBatchSandboxName is the name of the source BatchSandbox.
SourceBatchSandboxName string `json:"sourceBatchSandboxName"`

// SourcePodName is the name of the source Pod.
// +optional
// +kubebuilder:validation:Optional
SourcePodName string `json:"sourcePodName,omitempty"`

// SourceNodeName is the node where the source Pod runs.
// +optional
// +kubebuilder:validation:Optional
SourceNodeName string `json:"sourceNodeName,omitempty"`

// SnapshotPushSecret is the Secret name for pushing to registry.
// +optional
SnapshotPushSecret string `json:"snapshotPushSecret,omitempty"`

// ResumeImagePullSecret is the Secret name for pulling snapshot during resume.
// +optional
ResumeImagePullSecret string `json:"resumeImagePullSecret,omitempty"`

// ResumeTemplate contains enough information to reconstruct BatchSandbox.
// +optional
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Schemaless
ResumeTemplate *runtime.RawExtension `json:"resumeTemplate,omitempty"`

// SnapshotRegistry is the OCI registry for snapshot images.
// +optional
// +kubebuilder:validation:Optional
SnapshotRegistry string `json:"snapshotRegistry,omitempty"`

// ContainerSnapshots holds per-container snapshot information.
// The controller fills this during resolution.
// +optional
// +kubebuilder:validation:Optional
ContainerSnapshots []ContainerSnapshot `json:"containerSnapshots,omitempty"`

// PausedAt is the timestamp when pause was initiated.
PausedAt metav1.Time `json:"pausedAt"`

// PauseVersion is incremented by the server to request a pause.
// Controller ACKs by setting status.pauseVersion to match when entering Committing phase.
PauseVersion int `json:"pauseVersion"`

// ResumeVersion is incremented by the server to request a resume.
// Controller ACKs by setting status.resumeVersion to match when starting resume.
ResumeVersion int `json:"resumeVersion"`
}

// SnapshotRecord represents a single pause or resume event in the snapshot history.
type SnapshotRecord struct {
// Action is "Pause" or "Resume".
Action string `json:"action"`
// Version is the pauseVersion or resumeVersion that triggered this action.
Version int `json:"version"`
// Timestamp is when this record was created.
Timestamp metav1.Time `json:"timestamp"`
// Message is a human-readable description of the event.
Message string `json:"message"`
}

// SandboxSnapshotStatus defines the observed state of SandboxSnapshot.
type SandboxSnapshotStatus struct {
// Phase indicates the current phase of the snapshot.
Phase SandboxSnapshotPhase `json:"phase,omitempty"`

// Message provides human-readable status information.
Message string `json:"message,omitempty"`

// ReadyAt is the timestamp when the snapshot became Ready.
ReadyAt *metav1.Time `json:"readyAt,omitempty"`

// ContainerSnapshots holds per-container snapshot results (filled by controller after push).
// +optional
ContainerSnapshots []ContainerSnapshot `json:"containerSnapshots,omitempty"`

// PauseVersion is ACKed by the controller when entering Committing phase.
PauseVersion int `json:"pauseVersion"`

// ResumeVersion is ACKed by the controller when starting resume.
ResumeVersion int `json:"resumeVersion"`

// History records each pause/resume cycle.
// +optional
History []SnapshotRecord `json:"history,omitempty"`
}

// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=sbxsnap
// +kubebuilder:printcolumn:name="PHASE",type="string",JSONPath=".status.phase"
// +kubebuilder:printcolumn:name="SANDBOX_ID",type="string",JSONPath=".spec.sandboxId"
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
type SandboxSnapshot struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec SandboxSnapshotSpec `json:"spec,omitempty"`
Status SandboxSnapshotStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true
type SandboxSnapshotList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []SandboxSnapshot `json:"items"`
}

func init() {
SchemeBuilder.Register(&SandboxSnapshot{}, &SandboxSnapshotList{})
}
Loading
Loading