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

enhance: add the ability to require compute classes #2476

Merged
merged 8 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading