Skip to content

Commit 8a09e02

Browse files
Support Claw CR idling (#1281)
* Support Claw CR idling
1 parent ce95271 commit 8a09e02

4 files changed

Lines changed: 140 additions & 3 deletions

File tree

deploy/crds/README.adoc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ Copy from a cluster with CNV operator installed
1010
Copy from a cluster with Ansible Automation Platform operator installed
1111

1212
== serving CRDs
13-
Copy from a cluster with Serverless operator installed
13+
Copy from a cluster with Serverless operator installed
14+
15+
== claws CRD
16+
Copy from the claw-operator repo (config/crd/bases/claw.sandbox.redhat.com_claws.yaml)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
kind: CustomResourceDefinition
2+
apiVersion: apiextensions.k8s.io/v1
3+
metadata:
4+
name: claws.claw.sandbox.redhat.com
5+
spec:
6+
group: claw.sandbox.redhat.com
7+
names:
8+
plural: claws
9+
singular: claw
10+
kind: Claw
11+
listKind: ClawList
12+
scope: Namespaced
13+
versions:
14+
- name: v1alpha1
15+
served: true
16+
storage: true
17+
schema:
18+
openAPIV3Schema:
19+
description: Claw is the Schema for the claws API
20+
type: object
21+
properties:
22+
apiVersion:
23+
type: string
24+
kind:
25+
type: string
26+
metadata:
27+
type: object
28+
spec:
29+
description: Spec defines the desired state of Claw
30+
type: object
31+
properties:
32+
idle:
33+
description: Scale down replicas to put Claw into an idle mode
34+
type: boolean
35+
status:
36+
description: Status defines the observed state of Claw
37+
type: object
38+
x-kubernetes-preserve-unknown-fields: true
39+
subresources:
40+
status: {}

test/e2e/parallel/user_workloads_test.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,21 @@ func TestIdlerAndPriorityClass(t *testing.T) {
3838
podsToIdle := prepareWorkloads(t, await.Member1(), idler.Name, wait.WithSandboxPriorityClass())
3939
podsNoise := prepareWorkloads(t, await.Member1(), idlerNoise.Name, wait.WithSandboxPriorityClass())
4040

41+
// Create a Claw workload only in the dev namespace (the one being idled).
42+
// Not added to prepareWorkloads to avoid exceeding the ClusterResourceQuota pod limit
43+
// when workloads are created in multiple namespaces for the same user.
44+
clawDeployment := createClaw(t, memberAwait, "test-idler-claw", idler.Name)
45+
podsToIdle, err := memberAwait.WaitForPods(t, idler.Name, len(podsToIdle)+int(*clawDeployment.Spec.Replicas),
46+
wait.PodRunning(), wait.WithPodLabel("idler", "idler"), wait.WithSandboxPriorityClass())
47+
require.NoError(t, err)
48+
4149
// Create more noise pods in non-user namespace
4250
memberAwait.CreateNamespace(t, "workloads-noise")
4351
externalNsPodsNoise := prepareWorkloads(t, await.Member1(), "workloads-noise", wait.WithOriginalPriorityClass())
4452

4553
// Set a short timeout for one of the idler to trigger pod idling
4654
// The idler is currently updating its status since it's already been idling the pods. So we need to keep trying to update.
47-
idler, err := wait.For(t, memberAwait.Awaitility, &toolchainv1alpha1.Idler{}).
55+
idler, err = wait.For(t, memberAwait.Awaitility, &toolchainv1alpha1.Idler{}).
4856
Update(idler.Name, memberAwait.Namespace, func(i *toolchainv1alpha1.Idler) {
4957
i.Spec.TimeoutSeconds = 5
5058
})
@@ -101,6 +109,10 @@ func TestIdlerAndPriorityClass(t *testing.T) {
101109
_, err = memberAwait.WaitForAAP(t, "test-idler-aap", idler.Name, clnt.Resource(aapRes), true)
102110
require.NoError(t, err)
103111

112+
// Wait for the Claw resource to be idled (spec.idle: true)
113+
_, err = memberAwait.WaitForClaw(t, "test-idler-claw", idler.Name, clnt.Resource(clawRes), true)
114+
require.NoError(t, err)
115+
104116
// Wait for the InferenceService to be deleted - the expected action to idle
105117
// the workload is by deleting the InferenceService that is old enough.
106118
// The pods are idled as well, which is verified in the previous step - after some time,
@@ -208,6 +220,7 @@ func createDeployment(t *testing.T, memberAwait *wait.MemberAwaitility, namespac
208220
}
209221

210222
var aapRes = schema.GroupVersionResource{Group: "aap.ansible.com", Version: "v1alpha1", Resource: "ansibleautomationplatforms"}
223+
var clawRes = schema.GroupVersionResource{Group: "claw.sandbox.redhat.com", Version: "v1alpha1", Resource: "claws"}
211224
var servingRuntimeRes = schema.GroupVersionResource{Group: "serving.kserve.io", Version: "v1alpha1", Resource: "servingruntimes"}
212225
var inferenceServiceRes = schema.GroupVersionResource{Group: "serving.kserve.io", Version: "v1beta1", Resource: "inferenceservices"}
213226
var dataVolumeRes = schema.GroupVersionResource{Group: "cdi.kubevirt.io", Version: "v1beta1", Resource: "datavolumes"}
@@ -244,6 +257,51 @@ func createAAP(t *testing.T, memberAwait *wait.MemberAwaitility, name, namespace
244257
return deployment
245258
}
246259

260+
// createClaw creates an instance of claws.claw.sandbox.redhat.com with one deployment owned by this instance
261+
// returns the underlying deployment
262+
func createClaw(t *testing.T, memberAwait *wait.MemberAwaitility, name, namespace string) *appsv1.Deployment {
263+
clnt, err := dynamic.NewForConfig(memberAwait.RestConfig)
264+
require.NoError(t, err)
265+
266+
claw := clawResource(name)
267+
createdClaw, err := clnt.Resource(clawRes).Namespace(namespace).Create(context.TODO(), claw, metav1.CreateOptions{})
268+
require.NoError(t, err)
269+
270+
replicas := int32(2)
271+
deployment := &appsv1.Deployment{
272+
ObjectMeta: metav1.ObjectMeta{
273+
Name: name,
274+
Namespace: namespace,
275+
},
276+
Spec: appsv1.DeploymentSpec{
277+
Selector: &metav1.LabelSelector{MatchLabels: selectorLabels(name)},
278+
Replicas: &replicas,
279+
Template: podTemplateSpec(name),
280+
},
281+
}
282+
err = controllerutil.SetOwnerReference(createdClaw, deployment, scheme.Scheme)
283+
require.NoError(t, err)
284+
err = memberAwait.Create(t, deployment)
285+
require.NoError(t, err)
286+
287+
return deployment
288+
}
289+
290+
func clawResource(name string) *unstructured.Unstructured {
291+
return &unstructured.Unstructured{
292+
Object: map[string]interface{}{
293+
"apiVersion": "claw.sandbox.redhat.com/v1alpha1",
294+
"kind": "Claw",
295+
"metadata": map[string]interface{}{
296+
"name": name,
297+
},
298+
"spec": map[string]interface{}{
299+
"idle": false,
300+
},
301+
},
302+
}
303+
}
304+
247305
func createReplicaSet(t *testing.T, memberAwait *wait.MemberAwaitility, namespace string) *appsv1.ReplicaSet {
248306
// Standalone ReplicaSet
249307
replicas := int32(2)

testsupport/wait/member.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
toolchainv1alpha1 "github.com/codeready-toolchain/api/api/v1alpha1"
1616
"github.com/codeready-toolchain/toolchain-common/pkg/test"
1717
appstudiov1 "github.com/codeready-toolchain/toolchain-e2e/testsupport/appstudio/api/v1alpha1"
18+
"github.com/codeready-toolchain/toolchain-e2e/testsupport/cleanup"
1819
"github.com/davecgh/go-spew/spew"
1920
"github.com/ghodss/yaml"
2021
quotav1 "github.com/openshift/api/quota/v1"
@@ -1415,6 +1416,16 @@ func (a *MemberAwaitility) Create(t *testing.T, obj client.Object) error {
14151416
})
14161417
}
14171418

1419+
// CreateWithCleanup tries to create the object until success and schedules cleanup at test end.
1420+
// Workaround for https://github.com/kubernetes/kubernetes/issues/67761
1421+
func (a *MemberAwaitility) CreateWithCleanup(t *testing.T, obj client.Object) error {
1422+
if err := a.Create(t, obj); err != nil {
1423+
return err
1424+
}
1425+
cleanup.AddCleanTasks(t, a.Client, obj)
1426+
return nil
1427+
}
1428+
14181429
// PodWaitCriterion a struct to compare with a given Pod
14191430
type PodWaitCriterion struct {
14201431
Match func(*corev1.Pod) bool
@@ -1525,7 +1536,7 @@ func (a *MemberAwaitility) WaitForAAP(t *testing.T, name, namespace string, aapR
15251536
var aap *unstructured.Unstructured
15261537
err := wait.PollUntilContextTimeout(context.TODO(), a.RetryInterval, a.Timeout, true, func(ctx context.Context) (bool, error) {
15271538
var err error
1528-
aap, err = aapRes.Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{})
1539+
aap, err = aapRes.Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
15291540
if err != nil {
15301541
if errors.IsNotFound(err) {
15311542
return false, nil
@@ -1541,6 +1552,31 @@ func (a *MemberAwaitility) WaitForAAP(t *testing.T, name, namespace string, aapR
15411552
return aap, err
15421553
}
15431554

1555+
// WaitForClaw waits for the Claw resource to reach the expected idle state (spec.idle)
1556+
func (a *MemberAwaitility) WaitForClaw(t *testing.T, name, namespace string, clawRes dynamic.NamespaceableResourceInterface, expectedIdled bool) (*unstructured.Unstructured, error) {
1557+
t.Logf("waiting for Claw '%s' in namespace '%s'", name, namespace)
1558+
var claw *unstructured.Unstructured
1559+
err := wait.PollUntilContextTimeout(context.TODO(), a.RetryInterval, a.Timeout, true, func(ctx context.Context) (bool, error) {
1560+
var err error
1561+
claw, err = clawRes.Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
1562+
if err != nil {
1563+
if errors.IsNotFound(err) {
1564+
return false, nil
1565+
}
1566+
return false, err
1567+
}
1568+
idled, found, err := unstructured.NestedBool(claw.UnstructuredContent(), "spec", "idle")
1569+
if err != nil {
1570+
return true, err
1571+
}
1572+
if !found {
1573+
return false, nil
1574+
}
1575+
return expectedIdled == idled, nil
1576+
})
1577+
return claw, err
1578+
}
1579+
15441580
// WaitUntilInferenceServiceDeleted waits for the InferenceService resource to be deleted (idled)
15451581
func (a *MemberAwaitility) WaitUntilInferenceServiceDeleted(t *testing.T, name, namespace string, inferenceServiceRes dynamic.NamespaceableResourceInterface) error {
15461582
t.Logf("waiting for InferenceService '%s' to be deleted in namespace '%s'", name, namespace)

0 commit comments

Comments
 (0)