Skip to content

Commit 770015f

Browse files
authored
Implement a simple sandbox controller using controller-runtime (#5)
* Implement a simple sandbox controller using controller-runtime * Address review comments: 1. Remove finalizer and rely on ownerrefs for cleanup 2. Set labels on the pod 3. Use label filter predicate for watches
1 parent 96463c9 commit 770015f

File tree

8 files changed

+358
-71
lines changed

8 files changed

+358
-71
lines changed

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
.PHONY: all
2+
all: generate build
3+
14
.PHONY: generate
25
generate: install-generate-tools
36
go generate
47

8+
.PHONY: install-generate-tools
59
install-generate-tools:
610
go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest
711
go install github.com/B1NARY-GR0UP/nwa@latest
12+
13+
.PHONY: build
14+
build:
15+
go build -o bin/manager cmd/main.go

api/v1alpha1/sandbox_types.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,8 @@ type SandboxSpec struct {
2929
// The following markers will use OpenAPI v3 schema to validate the value
3030
// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html
3131

32-
// State: RUNNING(default)|SUSPENDED
33-
// +kubebuilder:default="RUNNING"
34-
// +kubebuilder:validation:Enum=RUNNING;SUSPENDED
35-
// +kubebuilder:validation:Required
36-
State string `json:"state" protobuf:"bytes,1,opt,name=state"`
37-
3832
// template is the object that describes the pod spec that will be used to create
39-
// an agent sandbox. Each pod will be named with the format sandbox.metadata.name
33+
// an agent sandbox.
4034
// +kubebuilder:validation:Required
4135
Template corev1.PodTemplateSpec `json:"template" protobuf:"bytes,3,opt,name=template"`
4236
}

cmd/main.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"flag"
21+
"os"
22+
23+
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
24+
// to ensure that exec-entrypoint and run can make use of them.
25+
_ "k8s.io/client-go/plugin/pkg/client/auth"
26+
27+
"k8s.io/apimachinery/pkg/runtime"
28+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
29+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
30+
ctrl "sigs.k8s.io/controller-runtime"
31+
"sigs.k8s.io/controller-runtime/pkg/healthz"
32+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
33+
34+
sandboxv1alpha1 "sigs.k8s.io/agent-sandbox/api/v1alpha1"
35+
"sigs.k8s.io/agent-sandbox/controllers"
36+
//+kubebuilder:scaffold:imports
37+
)
38+
39+
var (
40+
scheme = runtime.NewScheme()
41+
setupLog = ctrl.Log.WithName("setup")
42+
)
43+
44+
func init() {
45+
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
46+
47+
utilruntime.Must(sandboxv1alpha1.AddToScheme(scheme))
48+
//+kubebuilder:scaffold:scheme
49+
}
50+
51+
func main() {
52+
var metricsAddr string
53+
var enableLeaderElection bool
54+
var probeAddr string
55+
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
56+
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
57+
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
58+
"Enable leader election for controller manager. "+
59+
"Enabling this will ensure there is only one active controller manager.")
60+
opts := zap.Options{
61+
Development: true,
62+
}
63+
opts.BindFlags(flag.CommandLine)
64+
flag.Parse()
65+
66+
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
67+
68+
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
69+
Scheme: scheme,
70+
HealthProbeBindAddress: probeAddr,
71+
LeaderElection: enableLeaderElection,
72+
LeaderElectionID: "a3317529.x-k8s.io",
73+
})
74+
if err != nil {
75+
setupLog.Error(err, "unable to start manager")
76+
os.Exit(1)
77+
}
78+
79+
if err = (&controllers.SandboxReconciler{
80+
Client: mgr.GetClient(),
81+
Scheme: mgr.GetScheme(),
82+
}).SetupWithManager(mgr); err != nil {
83+
setupLog.Error(err, "unable to create controller", "controller", "Sandbox")
84+
os.Exit(1)
85+
}
86+
//+kubebuilder:scaffold:builder
87+
88+
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
89+
setupLog.Error(err, "unable to set up health check")
90+
os.Exit(1)
91+
}
92+
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
93+
setupLog.Error(err, "unable to set up ready check")
94+
os.Exit(1)
95+
}
96+
97+
setupLog.Info("starting manager")
98+
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
99+
setupLog.Error(err, "problem running manager")
100+
os.Exit(1)
101+
}
102+
}

controllers/sandbox_controller.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
22+
corev1 "k8s.io/api/core/v1"
23+
"k8s.io/apimachinery/pkg/api/errors"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/runtime"
26+
"k8s.io/apimachinery/pkg/types"
27+
ctrl "sigs.k8s.io/controller-runtime"
28+
"sigs.k8s.io/controller-runtime/pkg/builder"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
30+
"sigs.k8s.io/controller-runtime/pkg/log"
31+
"sigs.k8s.io/controller-runtime/pkg/predicate"
32+
33+
sandboxv1alpha1 "sigs.k8s.io/agent-sandbox/api/v1alpha1"
34+
)
35+
36+
// SandboxReconciler reconciles a Sandbox object
37+
type SandboxReconciler struct {
38+
client.Client
39+
Scheme *runtime.Scheme
40+
}
41+
42+
//+kubebuilder:rbac:groups=agents.x-k8s.io,resources=sandboxes,verbs=get;list;watch;create;update;patch;delete
43+
//+kubebuilder:rbac:groups=agents.x-k8s.io,resources=sandboxes/status,verbs=get;update;patch
44+
45+
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
46+
47+
// Reconcile is part of the main kubernetes reconciliation loop which aims to
48+
// move the current state of the cluster closer to the desired state.
49+
// TODO(user): Modify the Reconcile function to compare the state specified by
50+
// the Sandbox object against the actual cluster state, and then
51+
// perform operations to make the cluster state reflect the state specified by
52+
// the user.
53+
//
54+
// For more details, check Reconcile and its Result here:
55+
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
56+
func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
57+
log := log.FromContext(ctx)
58+
59+
sandbox := &sandboxv1alpha1.Sandbox{}
60+
if err := r.Get(ctx, req.NamespacedName, sandbox); err != nil {
61+
if errors.IsNotFound(err) {
62+
log.Info("sandbox resource not found. Ignoring since object must be deleted")
63+
return ctrl.Result{}, nil
64+
}
65+
return ctrl.Result{}, err
66+
}
67+
68+
if !sandbox.ObjectMeta.DeletionTimestamp.IsZero() {
69+
log.Info("Sandbox is being deleted")
70+
return ctrl.Result{}, nil
71+
}
72+
73+
// Check if the pod already exists, if not create a new one
74+
podInCluster := &corev1.Pod{}
75+
found := true
76+
err := r.Get(ctx, types.NamespacedName{Name: sandbox.Name, Namespace: sandbox.Namespace}, podInCluster)
77+
if err != nil {
78+
if !errors.IsNotFound(err) {
79+
log.Error(err, "Failed to get Pod")
80+
return ctrl.Result{}, err
81+
}
82+
found = false
83+
}
84+
85+
// Create a pod object from the sandbox
86+
pod, err := r.podForSandbox(sandbox)
87+
if err != nil {
88+
return ctrl.Result{}, err
89+
}
90+
if !found {
91+
log.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
92+
if err = r.Create(ctx, pod, client.FieldOwner("sandbox-controller")); err != nil {
93+
log.Error(err, "Failed to create", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
94+
return ctrl.Result{}, err
95+
}
96+
} else {
97+
log.Info("Found Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
98+
// TODO - Do we enfore (change) spec if a pod exists ?
99+
// r.Patch(ctx, pod, client.Apply, client.ForceOwnership, client.FieldOwner("sandbox-controller"))
100+
}
101+
return ctrl.Result{}, nil
102+
}
103+
104+
func (r *SandboxReconciler) podForSandbox(s *sandboxv1alpha1.Sandbox) (*corev1.Pod, error) {
105+
// TODO we need to handle this better.
106+
// We are enforcing the length limitation of label values
107+
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/
108+
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
109+
labelValue := s.Name
110+
if len(labelValue) > 63 {
111+
labelValue = labelValue[:63]
112+
}
113+
pod := &corev1.Pod{
114+
ObjectMeta: metav1.ObjectMeta{
115+
Name: s.Name,
116+
Namespace: s.Namespace,
117+
Labels: map[string]string{
118+
"agents.x-k8s.io/sandbox-name": labelValue,
119+
},
120+
},
121+
Spec: s.Spec.Template.Spec,
122+
}
123+
pod.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Pod"))
124+
if err := ctrl.SetControllerReference(s, pod, r.Scheme); err != nil {
125+
return nil, err
126+
}
127+
return pod, nil
128+
}
129+
130+
// SetupWithManager sets up the controller with the Manager.
131+
func (r *SandboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
132+
labelSelectorPredicate := predicate.NewPredicateFuncs(func(object client.Object) bool {
133+
// Filter for pods with the agent label
134+
if pod, ok := object.(*corev1.Pod); ok {
135+
if _, exists := pod.Labels["agents.x-k8s.io/sandbox-name"]; exists {
136+
return true
137+
}
138+
}
139+
return false
140+
})
141+
return ctrl.NewControllerManagedBy(mgr).
142+
For(&sandboxv1alpha1.Sandbox{}).
143+
Owns(&corev1.Pod{}, builder.WithPredicates(labelSelectorPredicate)).
144+
Complete(r)
145+
}

examples/sandbox.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: agents.x-k8s.io/v1alpha1
2+
kind: Sandbox
3+
metadata:
4+
name: sandbox-example
5+
spec:
6+
template:
7+
spec:
8+
containers:
9+
- name: my-container
10+
image: busybox
11+
command: ["/bin/sh", "-c", "sleep 3600"]

go.mod

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,66 @@
1-
module agents.x-k8s.io/sandbox
1+
module sigs.k8s.io/agent-sandbox
22

33
go 1.24.4
44

55
require (
66
k8s.io/api v0.33.4
77
k8s.io/apimachinery v0.33.4
8+
k8s.io/client-go v0.33.0
89
sigs.k8s.io/controller-runtime v0.21.0
910
)
1011

1112
require (
12-
github.com/fatih/color v1.18.0 // indirect
13+
github.com/beorn7/perks v1.0.1 // indirect
14+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
15+
github.com/davecgh/go-spew v1.1.1 // indirect
16+
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
17+
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
18+
github.com/fsnotify/fsnotify v1.7.0 // indirect
1319
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
1420
github.com/go-logr/logr v1.4.2 // indirect
21+
github.com/go-logr/zapr v1.3.0 // indirect
1522
github.com/go-openapi/jsonpointer v0.21.0 // indirect
1623
github.com/go-openapi/jsonreference v0.20.2 // indirect
1724
github.com/go-openapi/swag v0.23.0 // indirect
18-
github.com/gobuffalo/flect v1.0.3 // indirect
1925
github.com/gogo/protobuf v1.3.2 // indirect
26+
github.com/google/btree v1.1.3 // indirect
2027
github.com/google/gnostic-models v0.6.9 // indirect
21-
github.com/inconshreveable/mousetrap v1.1.0 // indirect
28+
github.com/google/go-cmp v0.7.0 // indirect
29+
github.com/google/uuid v1.6.0 // indirect
2230
github.com/josharian/intern v1.0.0 // indirect
2331
github.com/json-iterator/go v1.1.12 // indirect
24-
github.com/kr/text v0.2.0 // indirect
2532
github.com/mailru/easyjson v0.7.7 // indirect
26-
github.com/mattn/go-colorable v0.1.13 // indirect
27-
github.com/mattn/go-isatty v0.0.20 // indirect
2833
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
2934
github.com/modern-go/reflect2 v1.0.2 // indirect
30-
github.com/spf13/cobra v1.9.1 // indirect
35+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
36+
github.com/onsi/ginkgo/v2 v2.23.3 // indirect
37+
github.com/onsi/gomega v1.37.0 // indirect
38+
github.com/pkg/errors v0.9.1 // indirect
39+
github.com/prometheus/client_golang v1.22.0 // indirect
40+
github.com/prometheus/client_model v0.6.1 // indirect
41+
github.com/prometheus/common v0.62.0 // indirect
42+
github.com/prometheus/procfs v0.15.1 // indirect
3143
github.com/spf13/pflag v1.0.6 // indirect
3244
github.com/x448/float16 v0.8.4 // indirect
33-
golang.org/x/mod v0.24.0 // indirect
45+
go.uber.org/multierr v1.11.0 // indirect
46+
go.uber.org/zap v1.27.0 // indirect
3447
golang.org/x/net v0.39.0 // indirect
48+
golang.org/x/oauth2 v0.27.0 // indirect
3549
golang.org/x/sync v0.13.0 // indirect
3650
golang.org/x/sys v0.32.0 // indirect
51+
golang.org/x/term v0.31.0 // indirect
3752
golang.org/x/text v0.24.0 // indirect
53+
golang.org/x/time v0.9.0 // indirect
3854
golang.org/x/tools v0.32.0 // indirect
55+
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
3956
google.golang.org/protobuf v1.36.5 // indirect
57+
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
4058
gopkg.in/inf.v0 v0.9.1 // indirect
41-
gopkg.in/yaml.v2 v2.4.0 // indirect
4259
gopkg.in/yaml.v3 v3.0.1 // indirect
4360
k8s.io/apiextensions-apiserver v0.33.0 // indirect
44-
k8s.io/code-generator v0.33.0 // indirect
45-
k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 // indirect
4661
k8s.io/klog/v2 v2.130.1 // indirect
4762
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
4863
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
49-
sigs.k8s.io/controller-tools v0.18.0 // indirect
5064
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
5165
sigs.k8s.io/randfill v1.0.0 // indirect
5266
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect

0 commit comments

Comments
 (0)