Skip to content

Commit 3610434

Browse files
committed
Merge branch 'main' into AddPDB
2 parents a43a579 + eaf4c74 commit 3610434

File tree

12 files changed

+374
-14
lines changed

12 files changed

+374
-14
lines changed

api/v1alpha1/sandbox_types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ type PodTemplate struct {
8585
}
8686

8787
type PersistentVolumeClaimTemplate struct {
88-
// Metadata is the Pod's metadata. Only labels and annotations are used.
88+
// Metadata is the PVC's metadata.
8989
// +kubebuilder:validation:Optional
9090
EmbeddedObjectMetadata `json:"metadata" protobuf:"bytes,3,opt,name=metadata"`
9191

92-
// Spec is the Pod's spec
92+
// Spec is the PVC's spec
9393
// +kubebuilder:validation:Required
9494
Spec corev1.PersistentVolumeClaimSpec `json:"spec" protobuf:"bytes,3,opt,name=spec"`
9595
}

controllers/sandbox_controller.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import (
4141
)
4242

4343
const (
44-
sandboxLabel = "agents.x-k8s.io/sandbox-name-hash"
44+
sandboxLabel = "agents.x-k8s.io/sandbox-name-hash"
45+
sandboxControllerFieldOwner = "sandbox-controller"
4546
)
4647

4748
var (
@@ -100,7 +101,7 @@ func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
100101
oldStatus := sandbox.Status.DeepCopy()
101102
var err error
102103

103-
expired, requeueAfter := r.checkSandboxExpiry(sandbox)
104+
expired, requeueAfter := checkSandboxExpiry(sandbox)
104105

105106
// Check if sandbox has expired
106107
if expired {
@@ -272,7 +273,7 @@ func (r *SandboxReconciler) reconcileService(ctx context.Context, sandbox *sandb
272273
return nil, fmt.Errorf("SetControllerReference for Service failed: %w", err)
273274
}
274275

275-
err := r.Create(ctx, service, client.FieldOwner("sandbox-controller"))
276+
err := r.Create(ctx, service, client.FieldOwner(sandboxControllerFieldOwner))
276277
if err != nil {
277278
log.Error(err, "Failed to create", "Service.Namespace", service.Namespace, "Service.Name", service.Name)
278279
return nil, err
@@ -357,7 +358,7 @@ func (r *SandboxReconciler) reconcilePod(ctx context.Context, sandbox *sandboxv1
357358
if err := ctrl.SetControllerReference(sandbox, pod, r.Scheme); err != nil {
358359
return nil, fmt.Errorf("SetControllerReference for Pod failed: %w", err)
359360
}
360-
if err := r.Create(ctx, pod, client.FieldOwner("sandbox-controller")); err != nil {
361+
if err := r.Create(ctx, pod, client.FieldOwner(sandboxControllerFieldOwner)); err != nil {
361362
log.Error(err, "Failed to create", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name)
362363
return nil, err
363364
}
@@ -383,7 +384,7 @@ func (r *SandboxReconciler) reconcilePVCs(ctx context.Context, sandbox *sandboxv
383384
if err := ctrl.SetControllerReference(sandbox, pvc, r.Scheme); err != nil {
384385
return fmt.Errorf("SetControllerReference for PVC failed: %w", err)
385386
}
386-
if err := r.Create(ctx, pvc, client.FieldOwner("sandbox-controller")); err != nil {
387+
if err := r.Create(ctx, pvc, client.FieldOwner(sandboxControllerFieldOwner)); err != nil {
387388
log.Error(err, "Failed to create PVC", "PVC.Namespace", sandbox.Namespace, "PVC.Name", pvcName)
388389
return err
389390
}
@@ -436,7 +437,7 @@ func (r *SandboxReconciler) handleSandboxExpiry(ctx context.Context, sandbox *sa
436437
// checks if the sandbox has expired
437438
// returns true if expired, false otherwise
438439
// if not expired, also returns the duration to requeue after
439-
func (r *SandboxReconciler) checkSandboxExpiry(sandbox *sandboxv1alpha1.Sandbox) (bool, time.Duration) {
440+
func checkSandboxExpiry(sandbox *sandboxv1alpha1.Sandbox) (bool, time.Duration) {
440441
if sandbox.Spec.ShutdownTime == nil {
441442
return false, 0
442443
}

controllers/sandbox_controller_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package controllers
1717
import (
1818
"errors"
1919
"testing"
20+
"time"
2021

2122
"github.com/google/go-cmp/cmp"
2223
"github.com/google/go-cmp/cmp/cmpopts"
@@ -607,3 +608,44 @@ func TestReconcilePod(t *testing.T) {
607608
})
608609
}
609610
}
611+
612+
func TestSandboxExpiry(t *testing.T) {
613+
testCases := []struct {
614+
name string
615+
shutdownTime *metav1.Time
616+
wantExpired bool
617+
wantRequeue bool
618+
}{
619+
{
620+
name: "nil shutdown time",
621+
shutdownTime: nil,
622+
wantExpired: false,
623+
wantRequeue: false,
624+
},
625+
{
626+
name: "shutdown time in future",
627+
shutdownTime: ptr.To(metav1.NewTime(time.Now().Add(2 * time.Hour))),
628+
wantExpired: false,
629+
wantRequeue: true,
630+
},
631+
{
632+
name: "shutdown time in past",
633+
shutdownTime: ptr.To(metav1.NewTime(time.Now().Add(-10 * time.Second))),
634+
wantExpired: true,
635+
wantRequeue: false,
636+
},
637+
}
638+
for _, tc := range testCases {
639+
t.Run(tc.name, func(t *testing.T) {
640+
sandbox := &sandboxv1alpha1.Sandbox{}
641+
sandbox.Spec.ShutdownTime = tc.shutdownTime
642+
expired, requeueAfter := checkSandboxExpiry(sandbox)
643+
require.Equal(t, tc.wantExpired, expired)
644+
if tc.wantRequeue {
645+
require.Greater(t, requeueAfter, time.Duration(0))
646+
} else {
647+
require.Equal(t, time.Duration(0), requeueAfter)
648+
}
649+
})
650+
}
651+
}

dev/ci/presubmits/test-e2e

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,13 @@ def main():
3838
if result.returncode != 0:
3939
return result.returncode
4040
result = subprocess.run([f"{repo_root}/dev/tools/test-e2e"])
41-
if result.returncode != 0:
42-
return result.returncode
43-
41+
42+
# Always create junit file whether tests pass or fail
4443
artifact_dir = os.getenv("ARTIFACTS")
4544
if artifact_dir:
4645
shutil.copy(f"{repo_root}/bin/e2e-junit.xml", f"{artifact_dir}/junit.xml")
4746

48-
return 0
47+
return result.returncode
4948

5049

5150
if __name__ == "__main__":

examples/composing-sandbox-nw-policies/rgd.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ spec:
1212
group: custom.agents.x-k8s.io
1313
kind: AgenticSandbox
1414
# Spec fields that users can provide.
15+
types:
16+
egressRule: object
1517
spec:
1618
image: string | default="nginx"
1719
service:
@@ -21,7 +23,7 @@ spec:
2123
ingress:
2224
podLabels: "map[string]string"
2325
namespaceLabels: "map[string]string"
24-
egress: object
26+
egress: "[]egressRule"
2527
ingress:
2628
enabled: boolean | default=false
2729
status:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/bash
2+
# Copyright 2025 The Kubernetes Authors.
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+
18+
export GEMINI_API_KEY=`cat /tokens/gemini`
19+
20+
set -x
21+
22+
23+
# protection against running gemini on an unpause
24+
# if gemini-promt.txt dont run gemini else create and run it
25+
if [ -f gemini-prompt.txt ]; then
26+
echo "gemini-prompt.txt exists, skipping gemini generation"
27+
else
28+
echo "gemini-prompt.txt does not exist, running gemini"
29+
echo "$AGENT_PROMPT" > gemini-prompt.txt
30+
gemini -y -p "$AGENT_PROMPT" > gemini-output.txt || true
31+
fi
32+
33+
#/usr/local/bin/code-server-entrypoint
34+
/usr/bin/code-server --auth=none --bind-addr=0.0.0.0:13337

examples/vscode-sandbox/vscode-sandbox.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@ spec:
1313
image: ghcr.io/coder/envbuilder
1414
env:
1515
# URL to the repository where the .devcontainer folder we want to load is located
16+
# for a branch append "#refs/heads/branch-name"
1617
- name: ENVBUILDER_GIT_URL
1718
value: "https://github.com/kubernetes-sigs/agent-sandbox.git"
1819
- name: ENVBUILDER_DEVCONTAINER_DIR
1920
value: "examples/vscode-sandbox"
2021
- name: ENVBUILDER_GIT_CLONE_SINGLE_BRANCH
2122
value: "true"
2223
- name: ENVBUILDER_INIT_SCRIPT
23-
value: "/usr/local/bin/code-server-entrypoint"
24+
value: "examples/vscode-sandbox/entrypoint.sh"
25+
- name: AGENT_PROMPT
26+
value: "You are an expert kubernetes developer who is helping with code reviews. Please look at the most recent commit and provide a review feedback. Dont make any code changes. Would you approve it ?"
27+
- name: ENVBUILDER_IGNORE_PATHS
28+
value: "/var/run,/product_uuid,/product_name,/tokens"
2429
volumeMounts:
2530
- mountPath: /workspaces
2631
name: workspaces-pvc

test/e2e/framework/client.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ type ClusterClient struct {
3535
client client.Client
3636
}
3737

38+
// Update an object that already exists on the cluster.
39+
func (cl *ClusterClient) Update(ctx context.Context, obj client.Object) error {
40+
cl.Helper()
41+
nn := types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}
42+
cl.Logf("Updating object %T (%s)", obj, nn.String())
43+
if err := cl.client.Update(ctx, obj); err != nil {
44+
return fmt.Errorf("update %T (%s): %w", obj, nn.String(), err)
45+
}
46+
return nil
47+
}
48+
3849
// CreateWithCleanup creates the specified object and cleans up the object after
3950
// the test completes.
4051
func (cl *ClusterClient) CreateWithCleanup(ctx context.Context, obj client.Object) error {
@@ -51,6 +62,9 @@ func (cl *ClusterClient) CreateWithCleanup(ctx context.Context, obj client.Objec
5162
if err := cl.client.Delete(context.Background(), obj); err != nil && !k8serrors.IsNotFound(err) {
5263
cl.Errorf("CreateWithCleanup %T (%s): %s", obj, nn.String(), err)
5364
}
65+
if err := cl.WaitForObjectNotFound(context.Background(), obj); err != nil {
66+
cl.Errorf("CreateWithCleanup %T (%s): %s", obj, nn.String(), err)
67+
}
5468
})
5569
return nil
5670
}
@@ -72,13 +86,35 @@ func (cl *ClusterClient) ValidateObject(ctx context.Context, obj client.Object,
7286
return nil
7387
}
7488

89+
// ValidateObjectNotFound verifies the specified object does not exist.
90+
func (cl *ClusterClient) ValidateObjectNotFound(ctx context.Context, obj client.Object) error {
91+
cl.Helper()
92+
nn := types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}
93+
cl.Logf("ValidateObjectNotFound %T (%s)", obj, nn.String())
94+
err := cl.client.Get(ctx, nn, obj)
95+
if err == nil { // object still exists - error
96+
return fmt.Errorf("ValidateObjectNotFound %T (%s): object still exists",
97+
obj, nn.String())
98+
} else if !k8serrors.IsNotFound(err) { // unexpected error
99+
return fmt.Errorf("ValidateObjectNotFound %T (%s): %w",
100+
obj, nn.String(), err)
101+
}
102+
return nil // happy path - object not found
103+
}
104+
75105
// WaitForObject waits for the specified object to exist and satisfy the provided
76106
// predicates.
77107
func (cl *ClusterClient) WaitForObject(ctx context.Context, obj client.Object, p ...predicates.ObjectPredicate) error {
78108
cl.Helper()
79109
// Static 30 second timeout, this can be adjusted if needed
80110
timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
81111
defer cancel()
112+
start := time.Now()
113+
nn := types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}
114+
defer func() {
115+
cl.Helper()
116+
cl.Logf("WaitForObject %T (%s) took %s", obj, nn, time.Since(start))
117+
}()
82118
var validationErr error
83119
for {
84120
select {
@@ -94,6 +130,33 @@ func (cl *ClusterClient) WaitForObject(ctx context.Context, obj client.Object, p
94130
}
95131
}
96132

133+
// WaitForObjectNotFound waits for the specified object to not exist.
134+
func (cl *ClusterClient) WaitForObjectNotFound(ctx context.Context, obj client.Object) error {
135+
cl.Helper()
136+
// Static 30 second timeout, this can be adjusted if needed
137+
timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
138+
defer cancel()
139+
start := time.Now()
140+
nn := types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}
141+
defer func() {
142+
cl.Helper()
143+
cl.Logf("WaitForObjectNotFound %T (%s) took %s", obj, nn, time.Since(start))
144+
}()
145+
var validationErr error
146+
for {
147+
select {
148+
case <-timeoutCtx.Done():
149+
return fmt.Errorf("timed out waiting for object: %w", validationErr)
150+
default:
151+
if validationErr = cl.ValidateObjectNotFound(timeoutCtx, obj); validationErr == nil {
152+
return nil
153+
}
154+
// Simple sleep for fixed duration (basic MVP)
155+
time.Sleep(time.Second)
156+
}
157+
}
158+
}
159+
97160
// validateAgentSandboxInstallation verifies agent-sandbox system components are
98161
// installed.
99162
func (cl *ClusterClient) validateAgentSandboxInstallation(ctx context.Context) error {

test/e2e/framework/context.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func NewTestContext(t *testing.T) *TestContext {
7070
client: cl,
7171
}
7272
t.Cleanup(func() {
73+
t.Helper()
7374
if err := th.afterEach(); err != nil {
7475
t.Error(err)
7576
}
@@ -82,10 +83,14 @@ func NewTestContext(t *testing.T) *TestContext {
8283

8384
// beforeEach runs before each test case is executed.
8485
func (th *TestContext) beforeEach() error {
86+
th.Helper()
8587
return th.validateAgentSandboxInstallation(context.Background())
8688
}
8789

8890
// afterEach runs after each test case is executed.
91+
//
92+
//nolint:unparam // remove nolint once this is implemented
8993
func (th *TestContext) afterEach() error {
94+
th.Helper()
9095
return nil // currently no-op, add functionality as needed
9196
}

test/e2e/framework/predicates/metadata.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,17 @@ func HasOwnerReferences(want []metav1.OwnerReference) ObjectPredicate {
6868
return nil
6969
}
7070
}
71+
72+
// NotDeleted verifies the object has no deletion timestamp
73+
func NotDeleted() ObjectPredicate {
74+
return func(obj client.Object) error {
75+
if obj == nil {
76+
return fmt.Errorf("object is nil")
77+
}
78+
deletionTimestamp := obj.GetDeletionTimestamp()
79+
if deletionTimestamp != nil {
80+
return fmt.Errorf("unexpected deletionTimestamp: %s", deletionTimestamp)
81+
}
82+
return nil
83+
}
84+
}

0 commit comments

Comments
 (0)