Skip to content

Commit

Permalink
Move to Application Names (#12)
Browse files Browse the repository at this point in the history
As opposed to IDs, as this is a 3rd party extensible thing, and it makes
more sense to reference names and not IDs for better UX.
  • Loading branch information
spjmurray authored Nov 21, 2024
1 parent aa5fc01 commit 45bc185
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,37 +57,15 @@ spec:
to install.
items:
properties:
application:
description: Application is a reference to the typed application.
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
namespace:
description: |-
Namespace is the namespace of resource being referenced
Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details.
(Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled.
type: string
required:
- kind
- name
type: object
name:
description: Name is the application name.
type: string
version:
description: Version is the version of the application.
pattern: ^v?[0-9]+(\.[0-9]+)?(\.[0-9]+)?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?$
type: string
required:
- application
- name
type: object
type: array
pause:
Expand Down
5 changes: 2 additions & 3 deletions pkg/apis/unikorn/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package v1alpha1
import (
unikornv1core "github.com/unikorn-cloud/core/pkg/apis/unikorn/v1alpha1"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -56,8 +55,8 @@ type ApplicationSetSpec struct {
}

type ApplicationSpec struct {
// Application is a reference to the typed application.
Application corev1.TypedObjectReference `json:"application"`
// Name is the application name.
Name string `json:"name"`
// Version is the version of the application.
Version *unikornv1core.SemanticVersion `json:"version,omitempty"`
}
Expand Down
1 change: 0 additions & 1 deletion pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 11 additions & 11 deletions pkg/provisioners/managers/application/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func (p *Provisioner) getKubernetesClient(ctx context.Context, traceName string)
return ctx, client, nil
}

func getApplications(ctx context.Context, cli client.Client, namespace string) (map[string]*unikornv1core.HelmApplication, error) {
func getApplications(ctx context.Context, cli client.Client, namespace string) (solver.ApplicationIndex, error) {
var applications unikornv1core.HelmApplicationList

options := &client.ListOptions{
Expand All @@ -133,30 +133,30 @@ func getApplications(ctx context.Context, cli client.Client, namespace string) (
return nil, err
}

applicationMap := map[string]*unikornv1core.HelmApplication{}
l := make([]*unikornv1core.HelmApplication, len(applications.Items))

for i, application := range applications.Items {
applicationMap[application.Name] = &applications.Items[i]
for i := range applications.Items {
l[i] = &applications.Items[i]
}

return applicationMap, nil
return solver.NewApplicationIndex(l...)
}

type schedulerVistor struct {
applications map[string]*unikornv1core.HelmApplication
applications solver.ApplicationIndex
appVersions map[string]solver.AppVersion
dependers map[string][]string
seen sat.Set[string]
order []solver.AppVersion
}

func (v *schedulerVistor) Visit(av solver.AppVersion, enqueue func(solver.AppVersion)) error {
v.seen.Add(av.ID)
v.seen.Add(av.Name)

v.order = append(v.order, av)

// Doea anyone depdend on me?
if dependers, ok := v.dependers[av.ID]; ok {
if dependers, ok := v.dependers[av.Name]; ok {
for _, depender := range dependers {
dependerAppVersion := v.appVersions[depender]

Expand Down Expand Up @@ -202,9 +202,9 @@ func Schedule(ctx context.Context, client client.Client, namespace string, solut
var roots []solver.AppVersion

for av := range solution.All() {
appVersions[av.ID] = av
appVersions[av.Name] = av

application := applications[av.ID]
application := applications[av.Name]

version, err := application.GetVersion(av.Version)
if err != nil {
Expand All @@ -218,7 +218,7 @@ func Schedule(ctx context.Context, client client.Client, namespace string, solut
}

for _, dep := range version.Dependencies {
dependers[dep.Name] = append(dependers[dep.Name], av.ID)
dependers[dep.Name] = append(dependers[dep.Name], av.Name)
}
}

Expand Down
90 changes: 54 additions & 36 deletions pkg/solver/solver.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

unikornv1 "github.com/unikorn-cloud/application/pkg/apis/unikorn/v1alpha1"
unikornv1core "github.com/unikorn-cloud/core/pkg/apis/unikorn/v1alpha1"
"github.com/unikorn-cloud/core/pkg/constants"
)

var (
Expand All @@ -37,19 +38,6 @@ var (
ErrConstraint = errors.New("constraint error")
)

// HelmApplicationVersionIterator simplifies iteration over application
// versions and returns an ordered list (newest to oldest), of semantic
// versions.
type HelmApplicationVersionIterator struct {
application *unikornv1core.HelmApplication
}

func NewHelmApplicationVersionIterator(application *unikornv1core.HelmApplication) *HelmApplicationVersionIterator {
return &HelmApplicationVersionIterator{
application: application,
}
}

type Queue[T any] struct {
items []T
}
Expand Down Expand Up @@ -112,33 +100,63 @@ func (g *GraphWalker[T]) Walk(visitor GraphVisitor[T]) error {
// AppVersion wraps up applicationID and version tuples in a comparable
// and easy to use form when interacting with the SAT solver.
type AppVersion struct {
ID string
Name string
Version unikornv1core.SemanticVersion
}

func NewAppVersion(id string, version unikornv1core.SemanticVersion) AppVersion {
func NewAppVersion(name string, version unikornv1core.SemanticVersion) AppVersion {
return AppVersion{
ID: id,
Name: name,
Version: version,
}
}

type ApplicationIndex map[string]*unikornv1core.HelmApplication

func NewApplicationIndex(applications ...*unikornv1core.HelmApplication) (ApplicationIndex, error) {
index := ApplicationIndex{}

for _, application := range applications {
if application.Labels == nil {
return nil, fmt.Errorf("%w: application ID %s has no labels", ErrResourceDependency, application.Name)
}

name, ok := application.Labels[constants.NameLabel]
if !ok {
return nil, fmt.Errorf("%w: application ID %s has no name", ErrResourceDependency, application.Name)
}

index[name] = application
}

return index, nil
}

func (i ApplicationIndex) Get(name string) (*unikornv1core.HelmApplication, error) {
application, ok := i[name]
if !ok {
return nil, fmt.Errorf("%w: unable to locate application %s", ErrResourceDependency, name)
}

return application, nil
}

type solverVisitor struct {
applications map[string]*unikornv1core.HelmApplication
applications ApplicationIndex
model *sat.Model[AppVersion]
}

//nolint:cyclop
func (v *solverVisitor) Visit(id string, enqueue func(string)) error {
application, ok := v.applications[id]
if !ok {
return fmt.Errorf("%w: unable to locate application %s", ErrResourceDependency, id)
func (v *solverVisitor) Visit(name string, enqueue func(string)) error {
application, err := v.applications.Get(name)
if err != nil {
return err
}

appVersions := make([]AppVersion, 0, len(application.Spec.Versions))

for version := range application.Versions() {
appVersions = append(appVersions, NewAppVersion(application.Name, version.Version))
appVersions = append(appVersions, NewAppVersion(name, version.Version))
}

// Only one version of the application may be installed at any time...
Expand All @@ -151,12 +169,12 @@ func (v *solverVisitor) Visit(id string, enqueue func(string)) error {
// version from being used. If a version is installed, it also implies any
// recommended packages should be installed too.
for version := range application.Versions() {
av := NewAppVersion(application.Name, version.Version)
av := NewAppVersion(name, version.Version)

for _, dependency := range version.Dependencies {
dependantApplication, ok := v.applications[dependency.Name]
if !ok {
return fmt.Errorf("%w: requested application %s not in catalog", ErrResourceDependency, dependency.Name)
dependantApplication, err := v.applications.Get(dependency.Name)
if err != nil {
return err
}

depVersions := make([]AppVersion, 0, len(dependantApplication.Spec.Versions))
Expand All @@ -173,9 +191,9 @@ func (v *solverVisitor) Visit(id string, enqueue func(string)) error {
}

for _, recommendation := range version.Recommends {
recommendedApplication, ok := v.applications[recommendation.Name]
if !ok {
return fmt.Errorf("%w: requested application %s not in catalog", ErrResourceDependency, recommendation.Name)
recommendedApplication, err := v.applications.Get(recommendation.Name)
if err != nil {
return err
}

recVersions := make([]AppVersion, 0, len(recommendedApplication.Spec.Versions))
Expand All @@ -199,7 +217,7 @@ func (v *solverVisitor) Visit(id string, enqueue func(string)) error {
// then we have a conflict, and have to backtrack and try again with another version.
// Unlike typical SAT solver problems, choosing a different version can have the fun
// effect of changing its dependencies!
func SolveApplicationSet(ctx context.Context, applications map[string]*unikornv1core.HelmApplication, applicationset *unikornv1.ApplicationSet) (sat.Set[AppVersion], error) {
func SolveApplicationSet(ctx context.Context, applications ApplicationIndex, applicationset *unikornv1.ApplicationSet) (sat.Set[AppVersion], error) {
// We're going to do an exhaustive walk of the dependency graph gathering
// all application/version tuples as variables, and also create any clauses along the way.
graph := NewGraphWalker[string]()
Expand All @@ -209,12 +227,12 @@ func SolveApplicationSet(ctx context.Context, applications map[string]*unikornv1
// Populate the work queue with any application IDs that are requested by the
// user and any clauses relevant to the solver.
for _, ref := range applicationset.Spec.Applications {
application, ok := applications[ref.Application.Name]
if !ok {
return nil, fmt.Errorf("%w: requested application %s not in catalog", ErrResourceDependency, ref.Application.Name)
application, err := applications.Get(ref.Name)
if err != nil {
return nil, err
}

graph.Enqueue(ref.Application.Name)
graph.Enqueue(ref.Name)

// Add a unit clause if an application version is specified.
if ref.Version != nil {
Expand All @@ -223,15 +241,15 @@ func SolveApplicationSet(ctx context.Context, applications map[string]*unikornv1
return nil, err
}

model.Unary(NewAppVersion(ref.Application.Name, *ref.Version))
model.Unary(NewAppVersion(ref.Name, *ref.Version))

continue
}

l := make([]AppVersion, 0, len(application.Spec.Versions))

for version := range application.Versions() {
l = append(l, NewAppVersion(application.Name, version.Version))
l = append(l, NewAppVersion(ref.Name, version.Version))
}

model.AtLeastOneOf(l...)
Expand Down
Loading

0 comments on commit 45bc185

Please sign in to comment.