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

Enable setting a default ComputeClass on projects #2470

Closed
wants to merge 10 commits into from
157 changes: 122 additions & 35 deletions integration/run/run_test.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/apis/api.acorn.io/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ type InfoList struct {

type Project v1.ProjectInstance

func (p *Project) NamespaceScoped() bool {
func (*Project) NamespaceScoped() bool {
return false
}

Expand Down
10 changes: 6 additions & 4 deletions pkg/apis/internal.acorn.io/v1/projectinstance.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ type ProjectInstance struct {
}

type ProjectInstanceSpec struct {
DefaultRegion string `json:"defaultRegion,omitempty"`
SupportedRegions []string `json:"supportedRegions,omitempty"`
DefaultComputeClass string `json:"defaultComputeClass,omitempty"`
DefaultRegion string `json:"defaultRegion,omitempty"`
SupportedRegions []string `json:"supportedRegions,omitempty"`
}

type ProjectInstanceStatus struct {
Namespace string `json:"namespace,omitempty"`
DefaultRegion string `json:"defaultRegion,omitempty"`
Namespace string `json:"namespace,omitempty"`
DefaultComputeClass string `json:"defaultComputeClass,omitempty"`
DefaultRegion string `json:"defaultRegion,omitempty"`
// SupportedRegions on the status field should be an explicit list of supported regions.
// That is, if the user specifies "*" for supported regions, then the status value should be the list of all regions.
// This is to avoid having to make another call to explicitly list all regions.
Expand Down
84 changes: 60 additions & 24 deletions pkg/apis/internal.admin.acorn.io/v1/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,56 @@ import (
"fmt"
"sort"

"sigs.k8s.io/controller-runtime/pkg/client"
internalv1 "github.com/acorn-io/runtime/pkg/apis/internal.acorn.io/v1"
"github.com/acorn-io/z"
apierrors "k8s.io/apimachinery/pkg/api/errors"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
)

func getCurrentClusterComputeClassDefault(ctx context.Context, c client.Client) (*ClusterComputeClassInstance, error) {
clusterComputeClasses := ClusterComputeClassInstanceList{}
if err := c.List(ctx, &clusterComputeClasses, &client.ListOptions{}); err != nil {
// GetDefaultComputeClassName gets the name of the effective default ComputeClass for a given project namespace.
// The precedence for picking the default ComputeClass is as follows:
// 1. Any ProjectComputeClassInstance (in the project namespace) or ClusterComputeClassInstance that is specified by the
// ProjectInstance's DefaultComputeClass field
// 2. The ProjectComputeClassInstance (in the project namespace) with a Default field set to true
// 3. The ClusterComputeClassInstance with a Default field set to true
//
// If no default ComputeClass is found, an empty string is returned.
// If the project specifies a default compute class that doesn't exist, an error is returned.
func GetDefaultComputeClassName(ctx context.Context, c kclient.Client, namespace string) (string, error) {
var project internalv1.ProjectInstance
if err := c.Get(ctx, kclient.ObjectKey{Name: namespace}, &project); err != nil {
return "", fmt.Errorf("failed to get project instance to determine default compute class: %w", err)
}

if projectSpecified := project.Status.DefaultComputeClass; projectSpecified != "" {
if err := lookupComputeClass(ctx, c, namespace, projectSpecified); err != nil {
if apierrors.IsNotFound(err) {
return "", fmt.Errorf("project specified default compute class [%v] does not exist: %w", projectSpecified, err)
}
return "", fmt.Errorf("failed to get project specified default compute class [%v]: %w", projectSpecified, err)
}

return projectSpecified, nil
}

if pcc, err := getCurrentProjectComputeClassDefault(ctx, c, namespace); err != nil {
return "", err
} else if pcc != nil && pcc.Name != "" {
return pcc.Name, nil
}

if ccc, err := getCurrentClusterComputeClassDefault(ctx, c); err != nil {
return "", err
} else if ccc != nil && ccc.Name != "" {
return ccc.Name, nil
}

return "", nil
}

func getCurrentClusterComputeClassDefault(ctx context.Context, c kclient.Client) (*ClusterComputeClassInstance, error) {
var clusterComputeClasses ClusterComputeClassInstanceList
if err := c.List(ctx, &clusterComputeClasses, &kclient.ListOptions{}); err != nil {
return nil, err
}

Expand All @@ -26,17 +70,17 @@ func getCurrentClusterComputeClassDefault(ctx context.Context, c client.Client)
"cannot establish defaults because two default computeclasses exist: %v and %v",
defaultCCC.Name, clusterComputeClass.Name)
}
t := clusterComputeClass // Create a new variable that isn't being iterated on to get a pointer
defaultCCC = &t

defaultCCC = z.Pointer(clusterComputeClass)
}
}

return defaultCCC, nil
}

func getCurrentProjectComputeClassDefault(ctx context.Context, c client.Client, namespace string) (*ProjectComputeClassInstance, error) {
projectComputeClasses := ProjectComputeClassInstanceList{}
if err := c.List(ctx, &projectComputeClasses, &client.ListOptions{Namespace: namespace}); err != nil {
func getCurrentProjectComputeClassDefault(ctx context.Context, c kclient.Client, namespace string) (*ProjectComputeClassInstance, error) {
var projectComputeClasses ProjectComputeClassInstanceList
if err := c.List(ctx, &projectComputeClasses, &kclient.ListOptions{Namespace: namespace}); err != nil {
return nil, err
}

Expand All @@ -52,27 +96,19 @@ func getCurrentProjectComputeClassDefault(ctx context.Context, c client.Client,
"cannot establish defaults because two default computeclasses exist: %v and %v",
defaultPCC.Name, projectComputeClass.Name)
}
t := projectComputeClass // Create a new variable that isn't being iterated on to get a pointer
defaultPCC = &t

defaultPCC = z.Pointer(projectComputeClass)
}
}

return defaultPCC, nil
}

func GetDefaultComputeClass(ctx context.Context, c client.Client, namespace string) (string, error) {
pcc, err := getCurrentProjectComputeClassDefault(ctx, c, namespace)
if err != nil {
return "", err
} else if pcc != nil {
return pcc.Name, nil
func lookupComputeClass(ctx context.Context, c kclient.Client, namespace, name string) error {
if err := c.Get(ctx, kclient.ObjectKey{Namespace: namespace, Name: name},
new(ProjectComputeClassInstance)); kclient.IgnoreNotFound(err) != nil {
return err
}

ccc, err := getCurrentClusterComputeClassDefault(ctx, c)
if err != nil {
return "", err
} else if ccc != nil {
return ccc.Name, nil
}
return "", nil
return c.Get(ctx, kclient.ObjectKey{Namespace: "", Name: name}, new(ClusterComputeClassInstance))
}
208 changes: 208 additions & 0 deletions pkg/apis/internal.admin.acorn.io/v1/default_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package v1_test

import (
"context"
"testing"

internalv1 "github.com/acorn-io/runtime/pkg/apis/internal.acorn.io/v1"
internaladminv1 "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1"
"github.com/acorn-io/runtime/pkg/scheme"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestGetDefaultComputeClass(t *testing.T) {
ctx := context.Background()

type args struct {
namespace string
client kclient.Client
}
type expected struct {
computeClassName string
error bool
}
for _, tt := range []struct {
name string
args args
expected expected
}{
{
name: "No defaults",
args: args{
namespace: "pcc-project",
client: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
&internalv1.ProjectInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "pcc-project",
},
},
&internaladminv1.ProjectComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "project-compute-class",
Namespace: "pcc-project",
},
},
&internaladminv1.ClusterComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-compute-class",
},
},
).Build(),
},
},
{
name: "Default cluster compute class",
args: args{
namespace: "pcc-project",
client: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
&internalv1.ProjectInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "pcc-project",
},
},
&internaladminv1.ProjectComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "other-project-compute-class",
Namespace: "pcc-project",
},
},
&internaladminv1.ProjectComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "project-compute-class",
Namespace: "pcc-project",
},
},
&internaladminv1.ClusterComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "other-cluster-compute-class",
},
},
&internaladminv1.ClusterComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-compute-class",
},
Default: true,
},
).Build(),
},
expected: expected{
computeClassName: "cluster-compute-class",
},
},
{
name: "Project compute classes take precedence over cluster compute classes",
args: args{
namespace: "pcc-project",
client: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
&internalv1.ProjectInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "pcc-project",
},
},
&internaladminv1.ProjectComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "other-project-compute-class",
Namespace: "pcc-project",
},
},
&internaladminv1.ProjectComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "project-compute-class",
Namespace: "pcc-project",
},
Default: true,
},
&internaladminv1.ClusterComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "other-cluster-compute-class",
},
},
&internaladminv1.ClusterComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-compute-class",
},
Default: true,
},
).Build(),
},
expected: expected{
computeClassName: "project-compute-class",
},
},
{
name: "Project specified compute class takes precedence over default project compute class",
args: args{
namespace: "pcc-project",
client: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
&internalv1.ProjectInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "pcc-project",
},
Status: internalv1.ProjectInstanceStatus{
DefaultComputeClass: "project-specified-default",
},
},
&internaladminv1.ProjectComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "self-specified-default",
Namespace: "pcc-project",
},
Default: true,
},
&internaladminv1.ClusterComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "project-specified-default",
},
},
).Build(),
},
expected: expected{
computeClassName: "project-specified-default",
},
},
{
name: "Project specified compute class not found",
args: args{
namespace: "pcc-project",
client: fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(
&internalv1.ProjectInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "pcc-project",
},
Status: internalv1.ProjectInstanceStatus{
DefaultComputeClass: "project-specified-default",
},
},
&internaladminv1.ClusterComputeClassInstance{
ObjectMeta: metav1.ObjectMeta{
Name: "self-specified-default",
},
Default: true,
},
).Build(),
},
expected: expected{
error: true,
},
},
} {
t.Run(tt.name, func(t *testing.T) {
kc := tt.args.client
if kc == nil {
kc = fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
}

actualComputeClassName, err := internaladminv1.GetDefaultComputeClassName(ctx, kc, tt.args.namespace)
if tt.expected.error {
require.Error(t, err)
} else {
require.NoError(t, err)
}

require.Equal(t, tt.expected.computeClassName, actualComputeClassName)
})
}
}
Loading
Loading