Skip to content

Commit 20753d9

Browse files
committed
add podset integration test
Signed-off-by: googs1025 <[email protected]>
1 parent 6cc4c52 commit 20753d9

File tree

4 files changed

+434
-0
lines changed

4 files changed

+434
-0
lines changed

api/orchestration/v1alpha1/stormservice_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ const (
157157
// InPlaceUpdateStormServiceStrategyType inplace updates the ReplicaSets
158158
InPlaceUpdateStormServiceStrategyType StormServiceUpdateStrategyType = "InPlaceUpdate"
159159
)
160+
160161
//+genclient
161162
//+kubebuilder:object:root=true
162163
//+kubebuilder:subresource:status
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
Copyright 2025 The Aibrix Team.
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 controller
18+
19+
import (
20+
"context"
21+
"time"
22+
23+
"github.com/onsi/ginkgo/v2"
24+
"github.com/onsi/gomega"
25+
corev1 "k8s.io/api/core/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
29+
orchestrationapi "github.com/vllm-project/aibrix/api/orchestration/v1alpha1"
30+
"github.com/vllm-project/aibrix/test/utils/validation"
31+
"github.com/vllm-project/aibrix/test/utils/wrapper"
32+
)
33+
34+
var _ = ginkgo.Describe("PodSet controller test", func() {
35+
var ns *corev1.Namespace
36+
37+
// update represents a test step: optional mutation + validation
38+
type update struct {
39+
updateFunc func(podset *orchestrationapi.PodSet)
40+
checkFunc func(context.Context, client.Client, *orchestrationapi.PodSet)
41+
}
42+
43+
ginkgo.BeforeEach(func() {
44+
ns = &corev1.Namespace{
45+
ObjectMeta: metav1.ObjectMeta{
46+
GenerateName: "test-podset-",
47+
},
48+
}
49+
gomega.Expect(k8sClient.Create(ctx, ns)).To(gomega.Succeed())
50+
// Ensure namespace is fully created
51+
gomega.Eventually(func() error {
52+
return k8sClient.Get(ctx, client.ObjectKeyFromObject(ns), ns)
53+
}, time.Second*3).Should(gomega.Succeed())
54+
})
55+
56+
ginkgo.AfterEach(func() {
57+
gomega.Expect(k8sClient.Delete(ctx, ns)).To(gomega.Succeed())
58+
})
59+
60+
// testValidatingCase defines a test case with initial setup and a series of updates
61+
type testValidatingCase struct {
62+
makePodSet func() *orchestrationapi.PodSet
63+
updates []*update
64+
}
65+
66+
ginkgo.DescribeTable("test PodSet creation and reconciliation",
67+
func(tc *testValidatingCase) {
68+
podset := tc.makePodSet()
69+
for _, update := range tc.updates {
70+
if update.updateFunc != nil {
71+
update.updateFunc(podset)
72+
}
73+
74+
// Fetch the latest PodSet after update
75+
fetched := &orchestrationapi.PodSet{}
76+
gomega.Eventually(func(g gomega.Gomega) {
77+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(podset), fetched)
78+
g.Expect(err).ToNot(gomega.HaveOccurred())
79+
}, time.Second*5, time.Millisecond*250).Should(gomega.Succeed())
80+
81+
// Run validation check
82+
if update.checkFunc != nil {
83+
update.checkFunc(ctx, k8sClient, fetched)
84+
}
85+
}
86+
},
87+
88+
ginkgo.Entry("normal PodSet create and update replicas",
89+
&testValidatingCase{
90+
makePodSet: func() *orchestrationapi.PodSet {
91+
podTemplate := corev1.PodTemplateSpec{
92+
ObjectMeta: metav1.ObjectMeta{
93+
Labels: map[string]string{
94+
"app": "nginx",
95+
},
96+
},
97+
Spec: corev1.PodSpec{
98+
Containers: []corev1.Container{
99+
{
100+
Name: "nginx",
101+
Image: "nginx:latest",
102+
},
103+
},
104+
},
105+
}
106+
107+
return wrapper.MakePodSet("podset-normal").
108+
Namespace(ns.Name).
109+
PodGroupSize(3).
110+
PodTemplate(podTemplate).
111+
Obj()
112+
},
113+
updates: []*update{
114+
{
115+
// create PodSet but all pod is not ready
116+
updateFunc: func(podset *orchestrationapi.PodSet) {
117+
// Step 1: Create the PodSet
118+
gomega.Expect(k8sClient.Create(ctx, podset)).To(gomega.Succeed())
119+
// Step 2: Wait for all Pods to be created
120+
validation.WaitForPodsCreated(ctx, k8sClient, ns.Name, podset.Name, 3)
121+
},
122+
checkFunc: func(ctx context.Context, k8sClient client.Client, podset *orchestrationapi.PodSet) {
123+
// Validate Spec
124+
validation.ValidatePodSetSpec(podset, 3, false)
125+
// Validate Status
126+
validation.ValidatePodSetStatus(ctx, k8sClient,
127+
podset, orchestrationapi.PodSetPhasePending, 3, 0)
128+
},
129+
},
130+
{
131+
// trigger PodSet all pods to ready
132+
updateFunc: func(podset *orchestrationapi.PodSet) {
133+
// Step 1: List all Pods
134+
validation.WaitForPodsCreated(ctx, k8sClient, ns.Name, podset.Name, 3)
135+
// Step 2: Patch all Pods to Running and Ready (simulate integration test environment)
136+
validation.MarkPodSetPodsReady(ctx, k8sClient, ns.Name, podset.Name)
137+
},
138+
checkFunc: func(ctx context.Context, k8sClient client.Client, podset *orchestrationapi.PodSet) {
139+
// Validate Spec
140+
validation.ValidatePodSetSpec(podset, 3, false)
141+
gomega.Expect(podset.Spec.PodGroupSize).To(gomega.Equal(int32(3)))
142+
// Validate Status
143+
validation.ValidatePodSetStatus(ctx, k8sClient,
144+
podset, orchestrationapi.PodSetPhaseReady, 3, 3)
145+
},
146+
},
147+
},
148+
},
149+
),
150+
// TODO: add more test case
151+
)
152+
})

test/utils/validation/podset.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
Copyright 2025 The Aibrix Team.
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 validation
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"time"
23+
24+
"github.com/onsi/gomega"
25+
corev1 "k8s.io/api/core/v1"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
28+
orchestrationapi "github.com/vllm-project/aibrix/api/orchestration/v1alpha1"
29+
"github.com/vllm-project/aibrix/pkg/controller/constants"
30+
)
31+
32+
func WaitForPodsCreated(ctx context.Context, k8sClient client.Client, ns, podSetLabel string, expected int) {
33+
gomega.Eventually(func(g gomega.Gomega) int {
34+
podList := &corev1.PodList{}
35+
g.Expect(k8sClient.List(ctx, podList,
36+
client.InNamespace(ns),
37+
client.MatchingLabels{constants.PodSetNameLabelKey: podSetLabel},
38+
)).To(gomega.Succeed())
39+
return len(podList.Items)
40+
}, time.Second*10, time.Millisecond*250).Should(gomega.Equal(expected))
41+
}
42+
43+
func MarkPodSetPodsReady(ctx context.Context, k8sClient client.Client, ns, podSetLabel string) {
44+
gomega.Eventually(func(g gomega.Gomega) {
45+
podList := &corev1.PodList{}
46+
g.Expect(k8sClient.List(ctx, podList,
47+
client.InNamespace(ns),
48+
client.MatchingLabels{constants.PodSetNameLabelKey: podSetLabel},
49+
)).To(gomega.Succeed())
50+
51+
for i := range podList.Items {
52+
pod := &podList.Items[i]
53+
if pod.DeletionTimestamp != nil {
54+
continue
55+
}
56+
pod.Status.Phase = corev1.PodRunning
57+
pod.Status.Conditions = []corev1.PodCondition{{
58+
Type: corev1.PodReady,
59+
Status: corev1.ConditionTrue,
60+
Reason: "TestReady",
61+
}}
62+
g.Expect(k8sClient.Status().Update(ctx, pod)).To(gomega.Succeed())
63+
}
64+
}, time.Second*5, time.Millisecond*250).Should(gomega.Succeed())
65+
}
66+
67+
func ValidatePodSetSpec(podset *orchestrationapi.PodSet, expectedPodGroupSize int32, expectedStateful bool) {
68+
gomega.Expect(podset.Spec.PodGroupSize).To(gomega.Equal(expectedPodGroupSize))
69+
gomega.Expect(podset.Spec.Stateful).To(gomega.Equal(expectedStateful))
70+
}
71+
72+
func ValidatePodSetStatus(ctx context.Context, k8sClient client.Client,
73+
podset *orchestrationapi.PodSet, expectedPhase orchestrationapi.PodSetPhase, expectedTotal, expectedReady int32) {
74+
gomega.Eventually(func() error {
75+
latest := &orchestrationapi.PodSet{}
76+
key := client.ObjectKeyFromObject(podset)
77+
if err := k8sClient.Get(ctx, key, latest); err != nil {
78+
return fmt.Errorf("failed to get latest PodSet: %w", err)
79+
}
80+
if latest.Status.Phase != expectedPhase {
81+
return fmt.Errorf("expected Phase=%s, got %s", expectedPhase, latest.Status.Phase)
82+
}
83+
if latest.Status.TotalPods != expectedTotal {
84+
return fmt.Errorf("expected TotalPods=%d, got %d", expectedTotal, latest.Status.TotalPods)
85+
}
86+
if latest.Status.ReadyPods != expectedReady {
87+
return fmt.Errorf("expected ReadyPods=%d, got %d", expectedReady, latest.Status.ReadyPods)
88+
}
89+
return nil
90+
}, time.Second*30, time.Millisecond*250).Should(
91+
gomega.Succeed(), "PodSet status validation failed")
92+
}

0 commit comments

Comments
 (0)