Skip to content
This repository was archived by the owner on Mar 16, 2024. It is now read-only.

Commit 8cedbfc

Browse files
author
Oscar Ward
authored
enhance: add the ability to require compute classes (#2476)
1 parent 91c803e commit 8cedbfc

File tree

11 files changed

+290
-22
lines changed

11 files changed

+290
-22
lines changed

docs/docs/100-reference/01-command-line/acorn_install.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ acorn install
6969
--record-builds Keep a record of each acorn build that happens
7070
--registry-cpu string The CPU to allocate to the registry in the format of <req>:<limit> (example 200m:1000m)
7171
--registry-memory string The memory to allocate to the registry in the format of <req>:<limit> (example 256Mi:1Gi)
72+
--require-compute-class Require applications to have a Compute Class set (default is false)
7273
--service-lb-annotation strings Annotation to add to the service of type LoadBalancer. Defaults to empty. (example key=value)
7374
--set-pod-security-enforce-profile Set the PodSecurity profile on created namespaces (default true)
7475
--skip-checks Bypass installation checks

integration/helper/config.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,30 @@ func SetIgnoreResourceRequirementsWithRestore(ctx context.Context, t *testing.T,
6969
t.Fatal(err)
7070
}
7171
}
72+
73+
func SetRequireComputeClassWithRestore(ctx context.Context, t *testing.T, kclient kclient.WithWatch) {
74+
t.Helper()
75+
76+
cfg, err := config.Get(ctx, kclient)
77+
if err != nil {
78+
t.Fatal(err)
79+
}
80+
81+
state := z.Dereference(cfg.RequireComputeClass)
82+
83+
cfg.RequireComputeClass = z.Pointer(true)
84+
85+
t.Cleanup(func() {
86+
cfg.RequireComputeClass = z.Pointer(state)
87+
88+
err = config.Set(ctx, kclient, cfg)
89+
if err != nil {
90+
t.Fatal(err)
91+
}
92+
})
93+
94+
err = config.Set(ctx, kclient, cfg)
95+
if err != nil {
96+
t.Fatal(err)
97+
}
98+
}

integration/run/run_test.go

Lines changed: 224 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -931,10 +931,198 @@ func TestDeployParam(t *testing.T) {
931931
assert.Equal(t, "5", appInstance.Status.AppSpec.Containers["foo"].Environment[0].Value)
932932
}
933933

934+
func TestRequireComputeClass(t *testing.T) {
935+
ctx := helper.GetCTX(t)
936+
937+
helper.StartController(t)
938+
c, _ := helper.ClientAndProject(t)
939+
kc := helper.MustReturn(kclient.Default)
940+
941+
helper.SetRequireComputeClassWithRestore(ctx, t, kc)
942+
943+
checks := []struct {
944+
name string
945+
noComputeClass bool
946+
testDataDirectory string
947+
computeClass adminv1.ProjectComputeClassInstance
948+
expected map[string]v1.Scheduling
949+
waitFor func(obj *v1.AppInstance) bool
950+
fail bool
951+
failMessage string
952+
}{
953+
{
954+
name: "no-computeclass",
955+
noComputeClass: true,
956+
testDataDirectory: "./testdata/simple",
957+
fail: true,
958+
failMessage: "compute class required but none configured",
959+
},
960+
{
961+
name: "valid",
962+
testDataDirectory: "./testdata/computeclass",
963+
computeClass: adminv1.ProjectComputeClassInstance{
964+
ObjectMeta: metav1.ObjectMeta{
965+
Name: "acorn-test-custom",
966+
Namespace: c.GetNamespace(),
967+
},
968+
CPUScaler: 0.25,
969+
Memory: adminv1.ComputeClassMemory{
970+
Min: "512Mi",
971+
Max: "1Gi",
972+
},
973+
Resources: &corev1.ResourceRequirements{
974+
Limits: corev1.ResourceList{
975+
"mygpu/nvidia": resource.MustParse("1"),
976+
}, Requests: corev1.ResourceList{
977+
"mygpu/nvidia": resource.MustParse("1"),
978+
}},
979+
SupportedRegions: []string{apiv1.LocalRegion},
980+
},
981+
expected: map[string]v1.Scheduling{"simple": {
982+
Requirements: corev1.ResourceRequirements{
983+
Limits: corev1.ResourceList{
984+
corev1.ResourceMemory: resource.MustParse("1Gi"),
985+
"mygpu/nvidia": resource.MustParse("1"),
986+
},
987+
Requests: corev1.ResourceList{
988+
corev1.ResourceMemory: resource.MustParse("1Gi"),
989+
corev1.ResourceCPU: resource.MustParse("250m"),
990+
"mygpu/nvidia": resource.MustParse("1"),
991+
},
992+
},
993+
Tolerations: []corev1.Toleration{
994+
{
995+
Key: tolerations.WorkloadTolerationKey,
996+
Operator: corev1.TolerationOpExists,
997+
},
998+
}},
999+
},
1000+
waitFor: func(obj *v1.AppInstance) bool {
1001+
return obj.Status.Condition(v1.AppInstanceConditionParsed).Success &&
1002+
obj.Status.Condition(v1.AppInstanceConditionScheduling).Success
1003+
},
1004+
},
1005+
{
1006+
name: "default",
1007+
testDataDirectory: "./testdata/simple",
1008+
computeClass: adminv1.ProjectComputeClassInstance{
1009+
ObjectMeta: metav1.ObjectMeta{
1010+
Name: "acorn-test-custom",
1011+
Namespace: c.GetNamespace(),
1012+
},
1013+
Default: true,
1014+
CPUScaler: 0.25,
1015+
Memory: adminv1.ComputeClassMemory{
1016+
Default: "512Mi",
1017+
Max: "1Gi",
1018+
Min: "512Mi",
1019+
},
1020+
SupportedRegions: []string{apiv1.LocalRegion},
1021+
},
1022+
expected: map[string]v1.Scheduling{"simple": {
1023+
Requirements: corev1.ResourceRequirements{
1024+
Limits: corev1.ResourceList{
1025+
corev1.ResourceMemory: resource.MustParse("512Mi")},
1026+
Requests: corev1.ResourceList{
1027+
corev1.ResourceMemory: resource.MustParse("512Mi"),
1028+
corev1.ResourceCPU: resource.MustParse("125m"),
1029+
},
1030+
},
1031+
Tolerations: []corev1.Toleration{
1032+
{
1033+
Key: tolerations.WorkloadTolerationKey,
1034+
Operator: corev1.TolerationOpExists,
1035+
},
1036+
}},
1037+
},
1038+
waitFor: func(obj *v1.AppInstance) bool {
1039+
return obj.Status.Condition(v1.AppInstanceConditionParsed).Success &&
1040+
obj.Status.Condition(v1.AppInstanceConditionScheduling).Success
1041+
},
1042+
},
1043+
}
1044+
1045+
for _, tt := range checks {
1046+
asClusterComputeClass := adminv1.ClusterComputeClassInstance(tt.computeClass)
1047+
// Perform the same test cases on both Project and Cluster ComputeClasses
1048+
for kind, computeClass := range map[string]crClient.Object{"projectcomputeclass": &tt.computeClass, "clustercomputeclass": &asClusterComputeClass} {
1049+
testcase := fmt.Sprintf("%v-%v", kind, tt.name)
1050+
t.Run(testcase, func(t *testing.T) {
1051+
if !tt.noComputeClass {
1052+
if err := kc.Create(ctx, computeClass); err != nil {
1053+
t.Fatal(err)
1054+
}
1055+
1056+
// Clean-up and gurantee the computeclass doesn't exist after this test run
1057+
t.Cleanup(func() {
1058+
if err := kc.Delete(context.Background(), computeClass); err != nil && !apierrors.IsNotFound(err) {
1059+
t.Fatal(err)
1060+
}
1061+
err := helper.EnsureDoesNotExist(ctx, func() (crClient.Object, error) {
1062+
lookingFor := computeClass
1063+
err := kc.Get(ctx, router.Key(computeClass.GetNamespace(), computeClass.GetName()), lookingFor)
1064+
return lookingFor, err
1065+
})
1066+
if err != nil {
1067+
t.Fatal(err)
1068+
}
1069+
})
1070+
}
1071+
1072+
image, err := c.AcornImageBuild(ctx, tt.testDataDirectory+"/Acornfile", &client.AcornImageBuildOptions{
1073+
Cwd: tt.testDataDirectory,
1074+
})
1075+
if err != nil {
1076+
t.Fatal(err)
1077+
}
1078+
1079+
// Assign a name for the test case so no collisions occur
1080+
app, err := c.AppRun(ctx, image.ID, &client.AppRunOptions{Name: testcase})
1081+
if err == nil && tt.fail {
1082+
t.Fatal("expected error, got nil")
1083+
} else if err != nil {
1084+
if !tt.fail {
1085+
t.Fatal(err)
1086+
}
1087+
assert.Contains(t, err.Error(), tt.failMessage)
1088+
}
1089+
1090+
// Clean-up and gurantee the app doesn't exist after this test run
1091+
if app != nil {
1092+
t.Cleanup(func() {
1093+
if err = kc.Delete(context.Background(), app); err != nil && !apierrors.IsNotFound(err) {
1094+
t.Fatal(err)
1095+
}
1096+
err := helper.EnsureDoesNotExist(ctx, func() (crClient.Object, error) {
1097+
lookingFor := app
1098+
err := kc.Get(ctx, router.Key(app.GetName(), app.GetNamespace()), lookingFor)
1099+
return lookingFor, err
1100+
})
1101+
if err != nil {
1102+
t.Fatal(err)
1103+
}
1104+
})
1105+
}
1106+
1107+
if tt.waitFor != nil {
1108+
appInstance := &v1.AppInstance{
1109+
ObjectMeta: metav1.ObjectMeta{
1110+
Name: app.Name,
1111+
Namespace: app.Namespace,
1112+
},
1113+
}
1114+
appInstance = helper.WaitForObject(t, kc.Watch, new(v1.AppInstanceList), appInstance, tt.waitFor)
1115+
assert.EqualValues(t, appInstance.Status.Scheduling, tt.expected, "generated scheduling rules are incorrect")
1116+
}
1117+
})
1118+
}
1119+
}
1120+
}
1121+
9341122
func TestUsingComputeClasses(t *testing.T) {
9351123
helper.StartController(t)
9361124
c, _ := helper.ClientAndProject(t)
937-
kclient := helper.MustReturn(kclient.Default)
1125+
kc := helper.MustReturn(kclient.Default)
9381126

9391127
ctx := helper.GetCTX(t)
9401128

@@ -1149,6 +1337,24 @@ func TestUsingComputeClasses(t *testing.T) {
11491337
},
11501338
fail: true,
11511339
},
1340+
{
1341+
name: "no-region",
1342+
testDataDirectory: "./testdata/computeclass",
1343+
computeClass: adminv1.ProjectComputeClassInstance{
1344+
ObjectMeta: metav1.ObjectMeta{
1345+
Name: "acorn-test-custom",
1346+
Namespace: c.GetNamespace(),
1347+
},
1348+
Default: true,
1349+
CPUScaler: 0.25,
1350+
Memory: adminv1.ComputeClassMemory{
1351+
Default: "512Mi",
1352+
Max: "1Gi",
1353+
Min: "512Mi",
1354+
},
1355+
},
1356+
fail: true,
1357+
},
11521358
{
11531359
name: "does-not-exist",
11541360
noComputeClass: true,
@@ -1164,18 +1370,18 @@ func TestUsingComputeClasses(t *testing.T) {
11641370
testcase := fmt.Sprintf("%v-%v", kind, tt.name)
11651371
t.Run(testcase, func(t *testing.T) {
11661372
if !tt.noComputeClass {
1167-
if err := kclient.Create(ctx, computeClass); err != nil {
1373+
if err := kc.Create(ctx, computeClass); err != nil {
11681374
t.Fatal(err)
11691375
}
11701376

11711377
// Clean-up and gurantee the computeclass doesn't exist after this test run
11721378
t.Cleanup(func() {
1173-
if err := kclient.Delete(context.Background(), computeClass); err != nil && !apierrors.IsNotFound(err) {
1379+
if err := kc.Delete(context.Background(), computeClass); err != nil && !apierrors.IsNotFound(err) {
11741380
t.Fatal(err)
11751381
}
11761382
err := helper.EnsureDoesNotExist(ctx, func() (crClient.Object, error) {
11771383
lookingFor := computeClass
1178-
err := kclient.Get(ctx, router.Key(computeClass.GetNamespace(), computeClass.GetName()), lookingFor)
1384+
err := kc.Get(ctx, router.Key(computeClass.GetNamespace(), computeClass.GetName()), lookingFor)
11791385
return lookingFor, err
11801386
})
11811387
if err != nil {
@@ -1204,12 +1410,12 @@ func TestUsingComputeClasses(t *testing.T) {
12041410
// Clean-up and gurantee the app doesn't exist after this test run
12051411
if app != nil {
12061412
t.Cleanup(func() {
1207-
if err = kclient.Delete(context.Background(), app); err != nil && !apierrors.IsNotFound(err) {
1413+
if err = kc.Delete(context.Background(), app); err != nil && !apierrors.IsNotFound(err) {
12081414
t.Fatal(err)
12091415
}
12101416
err := helper.EnsureDoesNotExist(ctx, func() (crClient.Object, error) {
12111417
lookingFor := app
1212-
err := kclient.Get(ctx, router.Key(app.GetName(), app.GetNamespace()), lookingFor)
1418+
err := kc.Get(ctx, router.Key(app.GetName(), app.GetNamespace()), lookingFor)
12131419
return lookingFor, err
12141420
})
12151421
if err != nil {
@@ -1225,7 +1431,7 @@ func TestUsingComputeClasses(t *testing.T) {
12251431
Namespace: app.Namespace,
12261432
},
12271433
}
1228-
appInstance = helper.WaitForObject(t, kclient.Watch, new(v1.AppInstanceList), appInstance, tt.waitFor)
1434+
appInstance = helper.WaitForObject(t, kc.Watch, new(v1.AppInstanceList), appInstance, tt.waitFor)
12291435
assert.EqualValues(t, appInstance.Status.Scheduling, tt.expected, "generated scheduling rules are incorrect")
12301436
}
12311437
})
@@ -1288,11 +1494,11 @@ func TestAppWithBadDefaultRegion(t *testing.T) {
12881494
helper.StartController(t)
12891495

12901496
ctx := helper.GetCTX(t)
1291-
kclient := helper.MustReturn(kclient.Default)
1497+
kc := helper.MustReturn(kclient.Default)
12921498
c, project := helper.ClientAndProject(t)
12931499

12941500
storageClasses := new(storagev1.StorageClassList)
1295-
err := kclient.List(ctx, storageClasses)
1501+
err := kc.List(ctx, storageClasses)
12961502
if err != nil || len(storageClasses.Items) == 0 {
12971503
t.Skip("No storage classes, so skipping TestAppWithBadDefaultRegion")
12981504
return
@@ -1307,11 +1513,11 @@ func TestAppWithBadDefaultRegion(t *testing.T) {
13071513
Default: true,
13081514
SupportedRegions: []string{"custom"},
13091515
}
1310-
if err = kclient.Create(ctx, &volumeClass); err != nil {
1516+
if err = kc.Create(ctx, &volumeClass); err != nil {
13111517
t.Fatal(err)
13121518
}
13131519
defer func() {
1314-
if err = kclient.Delete(context.Background(), &volumeClass); err != nil && !apierrors.IsNotFound(err) {
1520+
if err = kc.Delete(context.Background(), &volumeClass); err != nil && !apierrors.IsNotFound(err) {
13151521
t.Fatal(err)
13161522
}
13171523
}()
@@ -1629,8 +1835,8 @@ func TestEnforcedQuota(t *testing.T) {
16291835
t.Fatal("error while getting rest config:", err)
16301836
}
16311837
// Create a project.
1632-
kclient := helper.MustReturn(kclient.Default)
1633-
project := helper.TempProject(t, kclient)
1838+
kc := helper.MustReturn(kclient.Default)
1839+
project := helper.TempProject(t, kc)
16341840

16351841
// Create a client for the project.
16361842
c, err := client.New(restConfig, project.Name, project.Name)
@@ -1644,7 +1850,7 @@ func TestEnforcedQuota(t *testing.T) {
16441850
obj.Annotations = make(map[string]string)
16451851
}
16461852
obj.Annotations[labels.ProjectEnforcedQuotaAnnotation] = "true"
1647-
return kclient.Update(ctx, obj) == nil
1853+
return kc.Update(ctx, obj) == nil
16481854
})
16491855

16501856
// Run a scaled app.
@@ -1673,7 +1879,7 @@ func TestEnforcedQuota(t *testing.T) {
16731879

16741880
// Grab the app's QuotaRequest and check that it has the appropriate values set.
16751881
quotaRequest := &adminv1.QuotaRequestInstance{}
1676-
err = kclient.Get(ctx, router.Key(app.Namespace, app.Name), quotaRequest)
1882+
err = kc.Get(ctx, router.Key(app.Namespace, app.Name), quotaRequest)
16771883
if err != nil {
16781884
t.Fatal(err)
16791885
}
@@ -1690,7 +1896,7 @@ func TestEnforcedQuota(t *testing.T) {
16901896
}},
16911897
AllocatedResources: quotaRequest.Spec.Resources,
16921898
}
1693-
err = kclient.Status().Update(ctx, quotaRequest)
1899+
err = kc.Status().Update(ctx, quotaRequest)
16941900
if err != nil {
16951901
t.Fatal(err)
16961902
}
@@ -1709,8 +1915,8 @@ func TestAutoUpgradeImageValidation(t *testing.T) {
17091915
if err != nil {
17101916
t.Fatal("error while getting rest config:", err)
17111917
}
1712-
kclient := helper.MustReturn(kclient.Default)
1713-
project := helper.TempProject(t, kclient)
1918+
kc := helper.MustReturn(kclient.Default)
1919+
project := helper.TempProject(t, kc)
17141920

17151921
c, err := client.New(restConfig, project.Name, project.Name)
17161922
if err != nil {

pkg/apis/api.acorn.io/v1/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@ type Config struct {
540540
RegistryMemory *string `json:"registryMemory" name:"registry-memory" usage:"The memory to allocate to the registry in the format of <req>:<limit> (example 256Mi:1Gi)"`
541541
RegistryCPU *string `json:"registryCPU" name:"registry-cpu" usage:"The CPU to allocate to the registry in the format of <req>:<limit> (example 200m:1000m)"`
542542
IgnoreResourceRequirements *bool `json:"ignoreResourceRequirements" name:"ignore-resource-requirements" usage:"Ignore memory and CPU requests and limits, intended for local development (default is false)"`
543+
RequireComputeClass *bool `json:"requireComputeClass" name:"require-compute-class" usage:"Require applications to have a Compute Class set (default is false)"`
543544
}
544545

545546
type EncryptionKey struct {

0 commit comments

Comments
 (0)