Skip to content

Commit 8611ddc

Browse files
authored
Improve coverage of unit tests (#61)
1 parent 221d27d commit 8611ddc

File tree

11 files changed

+208
-131
lines changed

11 files changed

+208
-131
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ vet: ## Run go vet against code.
8888

8989
.PHONY: test
9090
test: manifests generate fmt vet envtest ## Run unit tests.
91-
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -v -ginkgo.v -coverprofile cover.out
91+
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./api/... ./internal/... ./pkg/...) -v -ginkgo.v -coverprofile cover.out
9292

9393
# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors.
9494
.PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up.

hack/e2e-util.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ function configure_cluster {
155155
echo "Installing cert-manager"
156156
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/$CERTMANAGER_VERSION/cert-manager.yaml
157157

158+
echo "Installing Kubeflow operator version $KUBEFLOW_VERSION"
159+
kubectl apply -k "github.com/kubeflow/training-operator/manifests/overlays/standalone?ref=$KUBEFLOW_VERSION"
160+
158161
# sleep to ensure cert-manager is fully functional
159162
echo "Waiting for pod in the cert-manager namespace to become ready"
160163
while [[ $(kubectl get pods -n cert-manager -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}' | tr ' ' '\n' | sort -u) != "True" ]]
@@ -163,9 +166,6 @@ function configure_cluster {
163166
done
164167
echo ""
165168

166-
echo "Installing Kubeflow operator version $KUBEFLOW_VERSION"
167-
kubectl apply -k "github.com/kubeflow/training-operator/manifests/overlays/standalone?ref=$KUBEFLOW_VERSION"
168-
169169
# Sleep until the kubeflow operator is running
170170
echo "Waiting for pods in the kueueflow namespace to become ready"
171171
while [[ $(kubectl get pods -n kubeflow -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}' | tr ' ' '\n' | sort -u) != "True" ]]

internal/controller/appwrapper/appwrapper_controller.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,13 @@ func (r *AppWrapperReconciler) Reconcile(ctx context.Context, req ctrl.Request)
160160
Type: string(workloadv1beta2.QuotaReserved),
161161
Status: metav1.ConditionTrue,
162162
Reason: string(workloadv1beta2.AppWrapperResuming),
163-
Message: "AppWrapper was unsuspended by Kueue",
163+
Message: "Suspend is false",
164164
})
165165
meta.SetStatusCondition(&aw.Status.Conditions, metav1.Condition{
166166
Type: string(workloadv1beta2.ResourcesDeployed),
167167
Status: metav1.ConditionTrue,
168168
Reason: string(workloadv1beta2.AppWrapperResuming),
169-
Message: "AppWrapper was unsuspended by Kueue",
169+
Message: "Suspend is false",
170170
})
171171
return r.updateStatus(ctx, aw, workloadv1beta2.AppWrapperResuming)
172172

@@ -180,7 +180,7 @@ func (r *AppWrapperReconciler) Reconcile(ctx context.Context, req ctrl.Request)
180180
meta.SetStatusCondition(&aw.Status.Conditions, metav1.Condition{
181181
Type: string(workloadv1beta2.PodsReady),
182182
Status: metav1.ConditionFalse,
183-
Reason: "CreatedFailed",
183+
Reason: "CreateFailed",
184184
Message: fmt.Sprintf("fatal error creating components: %v", err),
185185
})
186186
return r.updateStatus(ctx, aw, workloadv1beta2.AppWrapperFailed) // abort on fatal error
@@ -244,14 +244,14 @@ func (r *AppWrapperReconciler) Reconcile(ctx context.Context, req ctrl.Request)
244244
Type: string(workloadv1beta2.ResourcesDeployed),
245245
Status: metav1.ConditionFalse,
246246
Reason: string(workloadv1beta2.AppWrapperSuspended),
247-
Message: "AppWrapper was suspended by Kueue",
247+
Message: "Suspend is true",
248248
})
249249
}
250250
meta.SetStatusCondition(&aw.Status.Conditions, metav1.Condition{
251251
Type: string(workloadv1beta2.QuotaReserved),
252252
Status: metav1.ConditionFalse,
253253
Reason: string(workloadv1beta2.AppWrapperSuspended),
254-
Message: "AppWrapper was suspended by Kueue",
254+
Message: "Suspend is true",
255255
})
256256
return r.updateStatus(ctx, aw, workloadv1beta2.AppWrapperSuspended)
257257

@@ -299,6 +299,7 @@ func parseComponent(aw *workloadv1beta2.AppWrapper, raw []byte) (*unstructured.U
299299
if namespace == "" {
300300
obj.SetNamespace(aw.Namespace)
301301
} else if namespace != aw.Namespace {
302+
// Should not happen, namespace equality checked by validateAppWrapperInvariants
302303
return nil, fmt.Errorf("component namespace \"%s\" is different from appwrapper namespace \"%s\"", namespace, aw.Namespace)
303304
}
304305
return obj, nil

internal/controller/appwrapper/appwrapper_controller_test.go

Lines changed: 77 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,34 @@ var _ = Describe("AppWrapper Controller", func() {
3838
var awReconciler *AppWrapperReconciler
3939
var awName types.NamespacedName
4040
markerPodSet := podset.PodSetInfo{
41-
Labels: map[string]string{"testkey1": "value1"},
42-
Annotations: map[string]string{"test2": "test2"},
41+
Labels: map[string]string{"testkey1": "value1"},
42+
Annotations: map[string]string{"test2": "test2"},
43+
NodeSelector: map[string]string{"nodeName": "myNode"},
44+
Tolerations: []v1.Toleration{{Key: "aKey", Operator: "Exists", Effect: "NoSchedule"}},
4345
}
4446
var kueuePodSets []kueue.PodSet
4547

46-
advanceToRunning := func() {
48+
advanceToResuming := func(components ...workloadv1beta2.AppWrapperComponent) {
49+
By("Create an AppWrapper")
50+
aw := toAppWrapper(components...)
51+
aw.Spec.Suspend = true
52+
Expect(k8sClient.Create(ctx, aw)).To(Succeed())
53+
awName = types.NamespacedName{
54+
Name: aw.Name,
55+
Namespace: aw.Namespace,
56+
}
57+
awReconciler = &AppWrapperReconciler{
58+
Client: k8sClient,
59+
Scheme: k8sClient.Scheme(),
60+
Config: &config.AppWrapperConfig{ManageJobsWithoutQueueName: true, StandaloneMode: false},
61+
}
62+
kueuePodSets = (*workload.AppWrapper)(aw).PodSets()
63+
4764
By("Reconciling: Empty -> Suspended")
4865
_, err := awReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awName})
4966
Expect(err).NotTo(HaveOccurred())
5067

51-
aw := getAppWrapper(awName)
68+
aw = getAppWrapper(awName)
5269
Expect(aw.Status.Phase).Should(Equal(workloadv1beta2.AppWrapperSuspended))
5370
Expect(controllerutil.ContainsFinalizer(aw, AppWrapperFinalizer)).Should(BeTrue())
5471

@@ -67,12 +84,14 @@ var _ = Describe("AppWrapper Controller", func() {
6784
Expect(meta.IsStatusConditionTrue(aw.Status.Conditions, string(workloadv1beta2.QuotaReserved))).Should(BeTrue())
6885
Expect((*workload.AppWrapper)(aw).IsActive()).Should(BeTrue())
6986
Expect((*workload.AppWrapper)(aw).IsSuspended()).Should(BeFalse())
87+
}
7088

89+
beginRunning := func() {
7190
By("Reconciling: Resuming -> Running")
72-
_, err = awReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awName})
91+
_, err := awReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awName})
7392
Expect(err).NotTo(HaveOccurred())
7493

75-
aw = getAppWrapper(awName)
94+
aw := getAppWrapper(awName)
7695
Expect(aw.Status.Phase).Should(Equal(workloadv1beta2.AppWrapperRunning))
7796
Expect(meta.IsStatusConditionTrue(aw.Status.Conditions, string(workloadv1beta2.ResourcesDeployed))).Should(BeTrue())
7897
Expect(meta.IsStatusConditionTrue(aw.Status.Conditions, string(workloadv1beta2.QuotaReserved))).Should(BeTrue())
@@ -83,10 +102,30 @@ var _ = Describe("AppWrapper Controller", func() {
83102
Expect(err).NotTo(HaveOccurred())
84103
Expect(podStatus.pending).Should(Equal(utils.ExpectedPodCount(aw)))
85104

105+
By("Simulating first Pod Running")
106+
Expect(setPodStatus(aw, v1.PodRunning, 1)).To(Succeed())
107+
By("Reconciling: Running -> Running")
108+
_, err = awReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awName})
109+
Expect(err).NotTo(HaveOccurred())
110+
111+
aw = getAppWrapper(awName)
112+
Expect(aw.Status.Phase).Should(Equal(workloadv1beta2.AppWrapperRunning))
113+
Expect(meta.IsStatusConditionTrue(aw.Status.Conditions, string(workloadv1beta2.ResourcesDeployed))).Should(BeTrue())
114+
Expect(meta.IsStatusConditionTrue(aw.Status.Conditions, string(workloadv1beta2.QuotaReserved))).Should(BeTrue())
115+
Expect(meta.IsStatusConditionTrue(aw.Status.Conditions, string(workloadv1beta2.PodsReady))).Should(BeFalse())
116+
Expect((*workload.AppWrapper)(aw).IsActive()).Should(BeTrue())
117+
Expect((*workload.AppWrapper)(aw).IsSuspended()).Should(BeFalse())
118+
podStatus, err = awReconciler.workloadStatus(ctx, aw)
119+
Expect(err).NotTo(HaveOccurred())
120+
Expect(podStatus.pending).Should(Equal(utils.ExpectedPodCount(aw) - 1))
121+
}
122+
123+
fullyRunning := func() {
124+
aw := getAppWrapper(awName)
86125
By("Simulating all Pods Running")
87126
Expect(setPodStatus(aw, v1.PodRunning, utils.ExpectedPodCount(aw))).To(Succeed())
88127
By("Reconciling: Running -> Running")
89-
_, err = awReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awName})
128+
_, err := awReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awName})
90129
Expect(err).NotTo(HaveOccurred())
91130

92131
aw = getAppWrapper(awName)
@@ -97,30 +136,13 @@ var _ = Describe("AppWrapper Controller", func() {
97136
Expect((*workload.AppWrapper)(aw).IsActive()).Should(BeTrue())
98137
Expect((*workload.AppWrapper)(aw).IsSuspended()).Should(BeFalse())
99138
Expect((*workload.AppWrapper)(aw).PodsReady()).Should(BeTrue())
100-
podStatus, err = awReconciler.workloadStatus(ctx, aw)
139+
podStatus, err := awReconciler.workloadStatus(ctx, aw)
101140
Expect(err).NotTo(HaveOccurred())
102141
Expect(podStatus.running).Should(Equal(utils.ExpectedPodCount(aw)))
103142
_, finished := (*workload.AppWrapper)(aw).Finished()
104143
Expect(finished).Should(BeFalse())
105144
}
106145

107-
BeforeEach(func() {
108-
By("Create an AppWrapper containing two Pods")
109-
aw := toAppWrapper(pod(100), pod(100))
110-
aw.Spec.Suspend = true
111-
Expect(k8sClient.Create(ctx, aw)).To(Succeed())
112-
awName = types.NamespacedName{
113-
Name: aw.Name,
114-
Namespace: aw.Namespace,
115-
}
116-
awReconciler = &AppWrapperReconciler{
117-
Client: k8sClient,
118-
Scheme: k8sClient.Scheme(),
119-
Config: &config.AppWrapperConfig{ManageJobsWithoutQueueName: true, StandaloneMode: false},
120-
}
121-
kueuePodSets = (*workload.AppWrapper)(aw).PodSets()
122-
})
123-
124146
AfterEach(func() {
125147
By("Cleanup the AppWrapper and ensure no Pods remain")
126148
aw := &workloadv1beta2.AppWrapper{}
@@ -139,7 +161,9 @@ var _ = Describe("AppWrapper Controller", func() {
139161
})
140162

141163
It("Happy Path Lifecycle", func() {
142-
advanceToRunning()
164+
advanceToResuming(pod(100), pod(100))
165+
beginRunning()
166+
fullyRunning()
143167

144168
By("Simulating one Pod Completing")
145169
aw := getAppWrapper(awName)
@@ -176,9 +200,11 @@ var _ = Describe("AppWrapper Controller", func() {
176200
})
177201

178202
It("Running Workloads can be Suspended", func() {
179-
advanceToRunning()
203+
advanceToResuming(pod(100), pod(100))
204+
beginRunning()
205+
fullyRunning()
180206

181-
By("Updating aw.Spec by invoking RunWithPodSetsInfo")
207+
By("Inoking Suspend and RestorePodSetsInfo")
182208
aw := getAppWrapper(awName)
183209
(*workload.AppWrapper)(aw).Suspend()
184210
Expect((*workload.AppWrapper)(aw).RestorePodSetsInfo(utilslices.Map(kueuePodSets, podset.FromPodSet))).To(BeTrue())
@@ -212,8 +238,10 @@ var _ = Describe("AppWrapper Controller", func() {
212238
Expect(podStatus.failed + podStatus.succeeded + podStatus.running + podStatus.pending).Should(Equal(int32(0)))
213239
})
214240

215-
It("A Pod Failure leads to a failed AppWrappers", func() {
216-
advanceToRunning()
241+
It("A Pod Failure leads to a failed AppWrapper", func() {
242+
advanceToResuming(pod(100), pod(100))
243+
beginRunning()
244+
fullyRunning()
217245

218246
By("Simulating one Pod Failing")
219247
aw := getAppWrapper(awName)
@@ -248,4 +276,23 @@ var _ = Describe("AppWrapper Controller", func() {
248276
Expect(finished).Should(BeTrue())
249277
})
250278

279+
It("Failure during resource creation leads to a failed AppWrapper", func() {
280+
advanceToResuming(pod(100), malformedPod(100))
281+
282+
By("Reconciling: Resuming -> Failed")
283+
_, err := awReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: awName})
284+
Expect(err).NotTo(HaveOccurred())
285+
286+
aw := getAppWrapper(awName)
287+
Expect(aw.Status.Phase).Should(Equal(workloadv1beta2.AppWrapperFailed))
288+
Expect(meta.IsStatusConditionTrue(aw.Status.Conditions, string(workloadv1beta2.ResourcesDeployed))).Should(BeTrue())
289+
Expect(meta.IsStatusConditionTrue(aw.Status.Conditions, string(workloadv1beta2.QuotaReserved))).Should(BeTrue())
290+
Expect(meta.IsStatusConditionTrue(aw.Status.Conditions, string(workloadv1beta2.PodsReady))).Should(BeFalse())
291+
Expect((*workload.AppWrapper)(aw).IsActive()).Should(BeTrue())
292+
Expect((*workload.AppWrapper)(aw).IsSuspended()).Should(BeFalse())
293+
podStatus, err := awReconciler.workloadStatus(ctx, aw)
294+
Expect(err).NotTo(HaveOccurred())
295+
Expect(podStatus.pending).Should(Equal(int32(1)))
296+
})
297+
251298
})

internal/controller/appwrapper/fixtures_test.go

Lines changed: 4 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func randName(baseName string) string {
4747

4848
func toAppWrapper(components ...workloadv1beta2.AppWrapperComponent) *workloadv1beta2.AppWrapper {
4949
return &workloadv1beta2.AppWrapper{
50+
TypeMeta: metav1.TypeMeta{APIVersion: workloadv1beta2.GroupVersion.String(), Kind: "AppWrapper"},
5051
ObjectMeta: metav1.ObjectMeta{Name: randName("aw"), Namespace: "default"},
5152
Spec: workloadv1beta2.AppWrapperSpec{Components: components},
5253
}
@@ -111,26 +112,23 @@ func pod(milliCPU int64) workloadv1beta2.AppWrapperComponent {
111112
}
112113
}
113114

114-
const namespacedPodYAML = `
115+
const malformedPodYAML = `
115116
apiVersion: v1
116117
kind: Pod
117118
metadata:
118119
name: %v
119-
namespace: %v
120120
spec:
121121
restartPolicy: Never
122122
containers:
123123
- name: busybox
124-
image: quay.io/project-codeflare/busybox:1.36
125124
command: ["sh", "-c", "sleep 10"]
126125
resources:
127126
requests:
128127
cpu: %v`
129128

130-
func namespacedPod(namespace string, milliCPU int64) workloadv1beta2.AppWrapperComponent {
131-
yamlString := fmt.Sprintf(namespacedPodYAML,
129+
func malformedPod(milliCPU int64) workloadv1beta2.AppWrapperComponent {
130+
yamlString := fmt.Sprintf(malformedPodYAML,
132131
randName("pod"),
133-
namespace,
134132
resource.NewMilliQuantity(milliCPU, resource.DecimalSI))
135133

136134
jsonBytes, err := yaml.YAMLToJSON([]byte(yamlString))
@@ -141,67 +139,3 @@ func namespacedPod(namespace string, milliCPU int64) workloadv1beta2.AppWrapperC
141139
Template: runtime.RawExtension{Raw: jsonBytes},
142140
}
143141
}
144-
145-
const serviceYAML = `
146-
apiVersion: v1
147-
kind: Service
148-
metadata:
149-
name: %v
150-
spec:
151-
selector:
152-
app: test
153-
ports:
154-
- protocol: TCP
155-
port: 80
156-
targetPort: 8080`
157-
158-
func service() workloadv1beta2.AppWrapperComponent {
159-
yamlString := fmt.Sprintf(serviceYAML, randName("service"))
160-
jsonBytes, err := yaml.YAMLToJSON([]byte(yamlString))
161-
Expect(err).NotTo(HaveOccurred())
162-
return workloadv1beta2.AppWrapperComponent{
163-
PodSets: []workloadv1beta2.AppWrapperPodSet{},
164-
Template: runtime.RawExtension{Raw: jsonBytes},
165-
}
166-
}
167-
168-
const deploymentYAML = `
169-
apiVersion: apps/v1
170-
kind: Deployment
171-
metadata:
172-
name: %v
173-
labels:
174-
app: test
175-
spec:
176-
replicas: %v
177-
selector:
178-
matchLabels:
179-
app: test
180-
template:
181-
metadata:
182-
labels:
183-
app: test
184-
spec:
185-
terminationGracePeriodSeconds: 0
186-
containers:
187-
- name: busybox
188-
image: quay.io/project-codeflare/busybox:1.36
189-
command: ["sh", "-c", "sleep 10000"]
190-
resources:
191-
requests:
192-
cpu: %v`
193-
194-
func deployment(replicaCount int, milliCPU int64) workloadv1beta2.AppWrapperComponent {
195-
yamlString := fmt.Sprintf(deploymentYAML,
196-
randName("deployment"),
197-
replicaCount,
198-
resource.NewMilliQuantity(milliCPU, resource.DecimalSI))
199-
200-
jsonBytes, err := yaml.YAMLToJSON([]byte(yamlString))
201-
Expect(err).NotTo(HaveOccurred())
202-
replicas := int32(replicaCount)
203-
return workloadv1beta2.AppWrapperComponent{
204-
PodSets: []workloadv1beta2.AppWrapperPodSet{{Replicas: &replicas, Path: "template.spec.template"}},
205-
Template: runtime.RawExtension{Raw: jsonBytes},
206-
}
207-
}

internal/controller/workload/workload_controller.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,7 @@ func (aw *AppWrapper) PodSets() []kueue.PodSet {
7878
continue // Should be unreachable; Template.Raw validated by our AdmissionController
7979
}
8080
for psIdx, podSet := range component.PodSets {
81-
replicas := int32(1)
82-
if podSet.Replicas != nil {
83-
replicas = *podSet.Replicas
84-
}
81+
replicas := utils.Replicas(podSet)
8582
if template, err := utils.GetPodTemplateSpec(obj, podSet.Path); err == nil {
8683
podSets = append(podSets, kueue.PodSet{
8784
Name: fmt.Sprintf("%s-%v-%v", aw.Name, componentIdx, psIdx),

0 commit comments

Comments
 (0)