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

Commit

Permalink
enhance: add the ability to require compute classes (#2476)
Browse files Browse the repository at this point in the history
  • Loading branch information
Oscar Ward authored Feb 12, 2024
1 parent 91c803e commit 8cedbfc
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 22 deletions.
1 change: 1 addition & 0 deletions docs/docs/100-reference/01-command-line/acorn_install.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ acorn install
--record-builds Keep a record of each acorn build that happens
--registry-cpu string The CPU to allocate to the registry in the format of <req>:<limit> (example 200m:1000m)
--registry-memory string The memory to allocate to the registry in the format of <req>:<limit> (example 256Mi:1Gi)
--require-compute-class Require applications to have a Compute Class set (default is false)
--service-lb-annotation strings Annotation to add to the service of type LoadBalancer. Defaults to empty. (example key=value)
--set-pod-security-enforce-profile Set the PodSecurity profile on created namespaces (default true)
--skip-checks Bypass installation checks
Expand Down
27 changes: 27 additions & 0 deletions integration/helper/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,30 @@ func SetIgnoreResourceRequirementsWithRestore(ctx context.Context, t *testing.T,
t.Fatal(err)
}
}

func SetRequireComputeClassWithRestore(ctx context.Context, t *testing.T, kclient kclient.WithWatch) {
t.Helper()

cfg, err := config.Get(ctx, kclient)
if err != nil {
t.Fatal(err)
}

state := z.Dereference(cfg.RequireComputeClass)

cfg.RequireComputeClass = z.Pointer(true)

t.Cleanup(func() {
cfg.RequireComputeClass = z.Pointer(state)

err = config.Set(ctx, kclient, cfg)
if err != nil {
t.Fatal(err)
}
})

err = config.Set(ctx, kclient, cfg)
if err != nil {
t.Fatal(err)
}
}
242 changes: 224 additions & 18 deletions integration/run/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -931,10 +931,198 @@ func TestDeployParam(t *testing.T) {
assert.Equal(t, "5", appInstance.Status.AppSpec.Containers["foo"].Environment[0].Value)
}

func TestRequireComputeClass(t *testing.T) {
ctx := helper.GetCTX(t)

helper.StartController(t)
c, _ := helper.ClientAndProject(t)
kc := helper.MustReturn(kclient.Default)

helper.SetRequireComputeClassWithRestore(ctx, t, kc)

checks := []struct {
name string
noComputeClass bool
testDataDirectory string
computeClass adminv1.ProjectComputeClassInstance
expected map[string]v1.Scheduling
waitFor func(obj *v1.AppInstance) bool
fail bool
failMessage string
}{
{
name: "no-computeclass",
noComputeClass: true,
testDataDirectory: "./testdata/simple",
fail: true,
failMessage: "compute class required but none configured",
},
{
name: "valid",
testDataDirectory: "./testdata/computeclass",
computeClass: adminv1.ProjectComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "acorn-test-custom",
Namespace: c.GetNamespace(),
},
CPUScaler: 0.25,
Memory: adminv1.ComputeClassMemory{
Min: "512Mi",
Max: "1Gi",
},
Resources: &corev1.ResourceRequirements{
Limits: corev1.ResourceList{
"mygpu/nvidia": resource.MustParse("1"),
}, Requests: corev1.ResourceList{
"mygpu/nvidia": resource.MustParse("1"),
}},
SupportedRegions: []string{apiv1.LocalRegion},
},
expected: map[string]v1.Scheduling{"simple": {
Requirements: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("1Gi"),
"mygpu/nvidia": resource.MustParse("1"),
},
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("1Gi"),
corev1.ResourceCPU: resource.MustParse("250m"),
"mygpu/nvidia": resource.MustParse("1"),
},
},
Tolerations: []corev1.Toleration{
{
Key: tolerations.WorkloadTolerationKey,
Operator: corev1.TolerationOpExists,
},
}},
},
waitFor: func(obj *v1.AppInstance) bool {
return obj.Status.Condition(v1.AppInstanceConditionParsed).Success &&
obj.Status.Condition(v1.AppInstanceConditionScheduling).Success
},
},
{
name: "default",
testDataDirectory: "./testdata/simple",
computeClass: adminv1.ProjectComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "acorn-test-custom",
Namespace: c.GetNamespace(),
},
Default: true,
CPUScaler: 0.25,
Memory: adminv1.ComputeClassMemory{
Default: "512Mi",
Max: "1Gi",
Min: "512Mi",
},
SupportedRegions: []string{apiv1.LocalRegion},
},
expected: map[string]v1.Scheduling{"simple": {
Requirements: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("512Mi")},
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("512Mi"),
corev1.ResourceCPU: resource.MustParse("125m"),
},
},
Tolerations: []corev1.Toleration{
{
Key: tolerations.WorkloadTolerationKey,
Operator: corev1.TolerationOpExists,
},
}},
},
waitFor: func(obj *v1.AppInstance) bool {
return obj.Status.Condition(v1.AppInstanceConditionParsed).Success &&
obj.Status.Condition(v1.AppInstanceConditionScheduling).Success
},
},
}

for _, tt := range checks {
asClusterComputeClass := adminv1.ClusterComputeClassInstance(tt.computeClass)
// Perform the same test cases on both Project and Cluster ComputeClasses
for kind, computeClass := range map[string]crClient.Object{"projectcomputeclass": &tt.computeClass, "clustercomputeclass": &asClusterComputeClass} {
testcase := fmt.Sprintf("%v-%v", kind, tt.name)
t.Run(testcase, func(t *testing.T) {
if !tt.noComputeClass {
if err := kc.Create(ctx, computeClass); err != nil {
t.Fatal(err)
}

// Clean-up and gurantee the computeclass doesn't exist after this test run
t.Cleanup(func() {
if err := kc.Delete(context.Background(), computeClass); err != nil && !apierrors.IsNotFound(err) {
t.Fatal(err)
}
err := helper.EnsureDoesNotExist(ctx, func() (crClient.Object, error) {
lookingFor := computeClass
err := kc.Get(ctx, router.Key(computeClass.GetNamespace(), computeClass.GetName()), lookingFor)
return lookingFor, err
})
if err != nil {
t.Fatal(err)
}
})
}

image, err := c.AcornImageBuild(ctx, tt.testDataDirectory+"/Acornfile", &client.AcornImageBuildOptions{
Cwd: tt.testDataDirectory,
})
if err != nil {
t.Fatal(err)
}

// Assign a name for the test case so no collisions occur
app, err := c.AppRun(ctx, image.ID, &client.AppRunOptions{Name: testcase})
if err == nil && tt.fail {
t.Fatal("expected error, got nil")
} else if err != nil {
if !tt.fail {
t.Fatal(err)
}
assert.Contains(t, err.Error(), tt.failMessage)
}

// Clean-up and gurantee the app doesn't exist after this test run
if app != nil {
t.Cleanup(func() {
if err = kc.Delete(context.Background(), app); err != nil && !apierrors.IsNotFound(err) {
t.Fatal(err)
}
err := helper.EnsureDoesNotExist(ctx, func() (crClient.Object, error) {
lookingFor := app
err := kc.Get(ctx, router.Key(app.GetName(), app.GetNamespace()), lookingFor)
return lookingFor, err
})
if err != nil {
t.Fatal(err)
}
})
}

if tt.waitFor != nil {
appInstance := &v1.AppInstance{
ObjectMeta: metav1.ObjectMeta{
Name: app.Name,
Namespace: app.Namespace,
},
}
appInstance = helper.WaitForObject(t, kc.Watch, new(v1.AppInstanceList), appInstance, tt.waitFor)
assert.EqualValues(t, appInstance.Status.Scheduling, tt.expected, "generated scheduling rules are incorrect")
}
})
}
}
}

func TestUsingComputeClasses(t *testing.T) {
helper.StartController(t)
c, _ := helper.ClientAndProject(t)
kclient := helper.MustReturn(kclient.Default)
kc := helper.MustReturn(kclient.Default)

ctx := helper.GetCTX(t)

Expand Down Expand Up @@ -1149,6 +1337,24 @@ func TestUsingComputeClasses(t *testing.T) {
},
fail: true,
},
{
name: "no-region",
testDataDirectory: "./testdata/computeclass",
computeClass: adminv1.ProjectComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "acorn-test-custom",
Namespace: c.GetNamespace(),
},
Default: true,
CPUScaler: 0.25,
Memory: adminv1.ComputeClassMemory{
Default: "512Mi",
Max: "1Gi",
Min: "512Mi",
},
},
fail: true,
},
{
name: "does-not-exist",
noComputeClass: true,
Expand All @@ -1164,18 +1370,18 @@ func TestUsingComputeClasses(t *testing.T) {
testcase := fmt.Sprintf("%v-%v", kind, tt.name)
t.Run(testcase, func(t *testing.T) {
if !tt.noComputeClass {
if err := kclient.Create(ctx, computeClass); err != nil {
if err := kc.Create(ctx, computeClass); err != nil {
t.Fatal(err)
}

// Clean-up and gurantee the computeclass doesn't exist after this test run
t.Cleanup(func() {
if err := kclient.Delete(context.Background(), computeClass); err != nil && !apierrors.IsNotFound(err) {
if err := kc.Delete(context.Background(), computeClass); err != nil && !apierrors.IsNotFound(err) {
t.Fatal(err)
}
err := helper.EnsureDoesNotExist(ctx, func() (crClient.Object, error) {
lookingFor := computeClass
err := kclient.Get(ctx, router.Key(computeClass.GetNamespace(), computeClass.GetName()), lookingFor)
err := kc.Get(ctx, router.Key(computeClass.GetNamespace(), computeClass.GetName()), lookingFor)
return lookingFor, err
})
if err != nil {
Expand Down Expand Up @@ -1204,12 +1410,12 @@ func TestUsingComputeClasses(t *testing.T) {
// Clean-up and gurantee the app doesn't exist after this test run
if app != nil {
t.Cleanup(func() {
if err = kclient.Delete(context.Background(), app); err != nil && !apierrors.IsNotFound(err) {
if err = kc.Delete(context.Background(), app); err != nil && !apierrors.IsNotFound(err) {
t.Fatal(err)
}
err := helper.EnsureDoesNotExist(ctx, func() (crClient.Object, error) {
lookingFor := app
err := kclient.Get(ctx, router.Key(app.GetName(), app.GetNamespace()), lookingFor)
err := kc.Get(ctx, router.Key(app.GetName(), app.GetNamespace()), lookingFor)
return lookingFor, err
})
if err != nil {
Expand All @@ -1225,7 +1431,7 @@ func TestUsingComputeClasses(t *testing.T) {
Namespace: app.Namespace,
},
}
appInstance = helper.WaitForObject(t, kclient.Watch, new(v1.AppInstanceList), appInstance, tt.waitFor)
appInstance = helper.WaitForObject(t, kc.Watch, new(v1.AppInstanceList), appInstance, tt.waitFor)
assert.EqualValues(t, appInstance.Status.Scheduling, tt.expected, "generated scheduling rules are incorrect")
}
})
Expand Down Expand Up @@ -1288,11 +1494,11 @@ func TestAppWithBadDefaultRegion(t *testing.T) {
helper.StartController(t)

ctx := helper.GetCTX(t)
kclient := helper.MustReturn(kclient.Default)
kc := helper.MustReturn(kclient.Default)
c, project := helper.ClientAndProject(t)

storageClasses := new(storagev1.StorageClassList)
err := kclient.List(ctx, storageClasses)
err := kc.List(ctx, storageClasses)
if err != nil || len(storageClasses.Items) == 0 {
t.Skip("No storage classes, so skipping TestAppWithBadDefaultRegion")
return
Expand All @@ -1307,11 +1513,11 @@ func TestAppWithBadDefaultRegion(t *testing.T) {
Default: true,
SupportedRegions: []string{"custom"},
}
if err = kclient.Create(ctx, &volumeClass); err != nil {
if err = kc.Create(ctx, &volumeClass); err != nil {
t.Fatal(err)
}
defer func() {
if err = kclient.Delete(context.Background(), &volumeClass); err != nil && !apierrors.IsNotFound(err) {
if err = kc.Delete(context.Background(), &volumeClass); err != nil && !apierrors.IsNotFound(err) {
t.Fatal(err)
}
}()
Expand Down Expand Up @@ -1629,8 +1835,8 @@ func TestEnforcedQuota(t *testing.T) {
t.Fatal("error while getting rest config:", err)
}
// Create a project.
kclient := helper.MustReturn(kclient.Default)
project := helper.TempProject(t, kclient)
kc := helper.MustReturn(kclient.Default)
project := helper.TempProject(t, kc)

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

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

// Grab the app's QuotaRequest and check that it has the appropriate values set.
quotaRequest := &adminv1.QuotaRequestInstance{}
err = kclient.Get(ctx, router.Key(app.Namespace, app.Name), quotaRequest)
err = kc.Get(ctx, router.Key(app.Namespace, app.Name), quotaRequest)
if err != nil {
t.Fatal(err)
}
Expand All @@ -1690,7 +1896,7 @@ func TestEnforcedQuota(t *testing.T) {
}},
AllocatedResources: quotaRequest.Spec.Resources,
}
err = kclient.Status().Update(ctx, quotaRequest)
err = kc.Status().Update(ctx, quotaRequest)
if err != nil {
t.Fatal(err)
}
Expand All @@ -1709,8 +1915,8 @@ func TestAutoUpgradeImageValidation(t *testing.T) {
if err != nil {
t.Fatal("error while getting rest config:", err)
}
kclient := helper.MustReturn(kclient.Default)
project := helper.TempProject(t, kclient)
kc := helper.MustReturn(kclient.Default)
project := helper.TempProject(t, kc)

c, err := client.New(restConfig, project.Name, project.Name)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/api.acorn.io/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ type Config struct {
RegistryMemory *string `json:"registryMemory" name:"registry-memory" usage:"The memory to allocate to the registry in the format of <req>:<limit> (example 256Mi:1Gi)"`
RegistryCPU *string `json:"registryCPU" name:"registry-cpu" usage:"The CPU to allocate to the registry in the format of <req>:<limit> (example 200m:1000m)"`
IgnoreResourceRequirements *bool `json:"ignoreResourceRequirements" name:"ignore-resource-requirements" usage:"Ignore memory and CPU requests and limits, intended for local development (default is false)"`
RequireComputeClass *bool `json:"requireComputeClass" name:"require-compute-class" usage:"Require applications to have a Compute Class set (default is false)"`
}

type EncryptionKey struct {
Expand Down
Loading

0 comments on commit 8cedbfc

Please sign in to comment.