diff --git a/api/v1alpha1/lease_helpers.go b/api/v1alpha1/lease_helpers.go new file mode 100644 index 00000000..d3589169 --- /dev/null +++ b/api/v1alpha1/lease_helpers.go @@ -0,0 +1,84 @@ +package v1alpha1 + +import ( + "context" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func (l *Lease) GetExporterSelector() (labels.Selector, error) { + return metav1.LabelSelectorAsSelector(&l.Spec.Selector) +} + +func (l *Lease) SetStatusPending(reason, messageFormat string, a ...any) { + l.SetStatusCondition(LeaseConditionTypePending, true, reason, messageFormat, a...) +} + +func (l *Lease) SetStatusReady(status bool, reason, messageFormat string, a ...any) { + l.SetStatusCondition(LeaseConditionTypeReady, status, reason, messageFormat, a...) +} + +func (l *Lease) SetStatusUnsatisfiable(reason, messageFormat string, a ...any) { + l.SetStatusCondition(LeaseConditionTypeUnsatisfiable, true, reason, messageFormat, a...) +} + +func (l *Lease) SetStatusInvalid(reason, messageFormat string, a ...any) { + l.SetStatusCondition(LeaseConditionTypeInvalid, true, reason, messageFormat, a...) +} + +func (l *Lease) SetStatusCondition( + condition LeaseConditionType, + status bool, + reason, messageFormat string, a ...any) { + + var statusCondition metav1.ConditionStatus + + if status { + statusCondition = metav1.ConditionTrue + } else { + statusCondition = metav1.ConditionFalse + } + + meta.SetStatusCondition(&l.Status.Conditions, metav1.Condition{ + Type: string(condition), + Status: statusCondition, + ObservedGeneration: l.Generation, + LastTransitionTime: metav1.Time{ + Time: time.Now(), + }, + Reason: reason, + Message: fmt.Sprintf(messageFormat, a...), + }) +} + +func (l *Lease) GetExporterName() string { + if l.Status.ExporterRef == nil { + return "(none)" + } + return l.Status.ExporterRef.Name +} + +func (l *Lease) GetClientName() string { + return l.Spec.ClientRef.Name +} + +func (l *Lease) Release(ctx context.Context) { + logger := log.FromContext(ctx) + logger.Info("The lease has been marked for release", "lease", l.Name, "exporter", l.GetExporterName(), "client", l.GetClientName()) + l.SetStatusReady(false, "Released", "The lease was marked for release") + l.Status.Ended = true + l.Status.EndTime = &metav1.Time{Time: time.Now()} +} + +func (l *Lease) Expire(ctx context.Context) { + logger := log.FromContext(ctx) + logger.Info("The lease has expired", "lease", l.Name, "exporter", l.GetExporterName(), "client", l.GetClientName()) + l.SetStatusReady(false, "Expired", "The lease has expired") + l.Status.Ended = true + l.Status.EndTime = &metav1.Time{Time: time.Now()} +} diff --git a/api/v1alpha1/lease_types.go b/api/v1alpha1/lease_types.go index f8434c71..5476c596 100644 --- a/api/v1alpha1/lease_types.go +++ b/api/v1alpha1/lease_types.go @@ -52,6 +52,7 @@ const ( LeaseConditionTypePending LeaseConditionType = "Pending" LeaseConditionTypeReady LeaseConditionType = "Ready" LeaseConditionTypeUnsatisfiable LeaseConditionType = "Unsatisfiable" + LeaseConditionTypeInvalid LeaseConditionType = "Invalid" ) type LeaseLabel string diff --git a/internal/controller/lease_controller.go b/internal/controller/lease_controller.go index 83eb5fc3..afe40118 100644 --- a/internal/controller/lease_controller.go +++ b/internal/controller/lease_controller.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "slices" + "strings" "time" jumpstarterdevv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1" @@ -41,6 +42,17 @@ type LeaseReconciler struct { Scheme *runtime.Scheme } +// ApprovedExporter represents an exporter that has been approved for leasing, +// along with its associated policy and any existing lease. +type ApprovedExporter struct { + // Exporter is the approved exporter + Exporter jumpstarterdevv1alpha1.Exporter + // ExistingLease is a pointer to any existing lease for this exporter, or nil if none exists + ExistingLease *jumpstarterdevv1alpha1.Lease + // Policy represents the access policy that approved this exporter + Policy jumpstarterdevv1alpha1.Policy +} + // +kubebuilder:rbac:groups=jumpstarter.dev,resources=leases,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=jumpstarter.dev,resources=leases/status,verbs=get;update;patch // +kubebuilder:rbac:groups=jumpstarter.dev,resources=leases/finalizers,verbs=update @@ -116,51 +128,30 @@ func (r *LeaseReconciler) reconcileStatusEnded( result *ctrl.Result, lease *jumpstarterdevv1alpha1.Lease, ) error { - logger := log.FromContext(ctx) now := time.Now() if !lease.Status.Ended { - if lease.Spec.Release { - logger.Info("reconcileStatusEndTime: force releasing lease") - meta.SetStatusCondition(&lease.Status.Conditions, metav1.Condition{ - Type: string(jumpstarterdevv1alpha1.LeaseConditionTypeReady), - Status: metav1.ConditionFalse, - ObservedGeneration: lease.Generation, - LastTransitionTime: metav1.Time{ - Time: now, - }, - Reason: "Released", - }) + // if lease has status condition unsatisfiable or invalid, we mark it as ended to avoid reprocessing + if meta.IsStatusConditionTrue(lease.Status.Conditions, string(jumpstarterdevv1alpha1.LeaseConditionTypeUnsatisfiable)) || + meta.IsStatusConditionTrue(lease.Status.Conditions, string(jumpstarterdevv1alpha1.LeaseConditionTypeInvalid)) { lease.Status.Ended = true - lease.Status.EndTime = &metav1.Time{ - Time: now, - } + lease.Status.EndTime = &metav1.Time{Time: now} + return nil + } else if lease.Spec.Release { + lease.Release(ctx) return nil } else if lease.Status.BeginTime != nil { expiration := lease.Status.BeginTime.Add(lease.Spec.Duration.Duration) if expiration.Before(now) { - logger.Info("reconcileStatusEndTime: lease expired") - meta.SetStatusCondition(&lease.Status.Conditions, metav1.Condition{ - Type: string(jumpstarterdevv1alpha1.LeaseConditionTypeReady), - Status: metav1.ConditionFalse, - ObservedGeneration: lease.Generation, - LastTransitionTime: metav1.Time{ - Time: time.Now(), - }, - Reason: "Expired", - }) - lease.Status.Ended = true - lease.Status.EndTime = &metav1.Time{ - Time: now, - } + lease.Expire(ctx) return nil } else { result.RequeueAfter = expiration.Sub(now) return nil } } - } + } return nil } @@ -173,16 +164,8 @@ func (r *LeaseReconciler) reconcileStatusBeginTime( now := time.Now() if lease.Status.BeginTime == nil && lease.Status.ExporterRef != nil { - logger.Info("reconcileStatusBeginTime: updating begin time") - meta.SetStatusCondition(&lease.Status.Conditions, metav1.Condition{ - Type: string(jumpstarterdevv1alpha1.LeaseConditionTypeReady), - Status: metav1.ConditionTrue, - ObservedGeneration: lease.Generation, - LastTransitionTime: metav1.Time{ - Time: now, - }, - Reason: "Ready", - }) + logger.Info("Updating begin time for lease", "lease", lease.Name, "exporter", lease.GetExporterName(), "client", lease.GetClientName()) + lease.SetStatusReady(true, "Ready", "An exporter has been acquired for the client") lease.Status.BeginTime = &metav1.Time{ Time: now, } @@ -199,158 +182,97 @@ func (r *LeaseReconciler) reconcileStatusExporterRef( ) error { logger := log.FromContext(ctx) + // Do not attempt to reconcile if the lease is already ended/invalid/etc + if lease.Status.Ended { + return nil + } + if lease.Status.ExporterRef == nil { - logger.Info("reconcileStatusExporterRef: looking for matching exporter") + logger.Info("Looking for a matching exporter for lease", "lease", lease.Name, "client", lease.GetClientName(), "selector", lease.Spec.Selector) - selector, err := metav1.LabelSelectorAsSelector(&lease.Spec.Selector) + selector, err := lease.GetExporterSelector() if err != nil { - return fmt.Errorf("reconcileStatusExporterRef: failed to create selector from label selector: %w", err) + return fmt.Errorf("reconcileStatusExporterRef: failed to get exporter selector: %w", err) + } else if selector.Empty() { + lease.SetStatusInvalid("InvalidSelector", "The selector for the lease is empty, a selector is required") + return nil } // List all Exporter matching selector - var matchingExporters jumpstarterdevv1alpha1.ExporterList - if err := r.List( - ctx, - &matchingExporters, - client.InNamespace(lease.Namespace), - client.MatchingLabelsSelector{Selector: selector}, - ); err != nil { - return fmt.Errorf("reconcileStatusExporterRef: failed to list exporters matching selector: %w", err) + matchingExporters, err := r.ListMatchingExporters(ctx, lease, selector) + if err != nil { + return fmt.Errorf("reconcileStatusExporterRef: failed to list matching exporters: %w", err) } // Filter out offline exporters - onlineExporters := slices.DeleteFunc( - matchingExporters.Items, - func(exporter jumpstarterdevv1alpha1.Exporter) bool { - return !(true && - meta.IsStatusConditionTrue( - exporter.Status.Conditions, - string(jumpstarterdevv1alpha1.ExporterConditionTypeRegistered), - ) && - meta.IsStatusConditionTrue( - exporter.Status.Conditions, - string(jumpstarterdevv1alpha1.ExporterConditionTypeOnline), - )) - }, - ) + onlineExporters := filterOutOfflineExporters(matchingExporters.Items) // No matching exporter online, lease unsatisfiable if len(onlineExporters) == 0 { - meta.SetStatusCondition(&lease.Status.Conditions, metav1.Condition{ - Type: string(jumpstarterdevv1alpha1.LeaseConditionTypeUnsatisfiable), - Status: metav1.ConditionTrue, - ObservedGeneration: lease.Generation, - LastTransitionTime: metav1.Time{ - Time: time.Now(), - }, - Reason: "NoExporter", - }) + lease.SetStatusUnsatisfiable( + "NoExporter", + "There are no online exporter matching the selector, but there are %d matching offline exporters", + len(matchingExporters.Items)) return nil } - var leases jumpstarterdevv1alpha1.LeaseList - if err := r.List( - ctx, - &leases, - client.InNamespace(lease.Namespace), - MatchingActiveLeases(), - ); err != nil { + approvedExporters, err := r.attachMatchingPolicies(ctx, lease, onlineExporters) + if err != nil { + return fmt.Errorf("reconcileStatusExporterRef: failed to handle policy approval: %w", err) + } + + if len(approvedExporters) == 0 { + lease.SetStatusUnsatisfiable( + "NoAccess", + "While there are %d online exporters matching the selector, none of them are approved by any policy for your client", + len(onlineExporters)) + return nil + } + // Filter out exporters that are already leased + activeLeases, err := r.ListActiveLeases(ctx, lease.Namespace) + if err != nil { return fmt.Errorf("reconcileStatusExporterRef: failed to list active leases: %w", err) } - availableExporters := slices.DeleteFunc(onlineExporters, func(exporter jumpstarterdevv1alpha1.Exporter) bool { - for _, existingLease := range leases.Items { - // if the lease is referencing the current exporter - if existingLease.Status.ExporterRef != nil && existingLease.Status.ExporterRef.Name == exporter.Name { - return true - } - } - return false - }) + approvedExporters = attachExistingLeases(approvedExporters, activeLeases.Items) + orderedExporters := orderApprovedExporters(approvedExporters) - var approvedExporters []struct { - Exporter jumpstarterdevv1alpha1.Exporter - Policy jumpstarterdevv1alpha1.Policy + if len(orderedExporters) > 0 && orderedExporters[0].Policy.SpotAccess { + lease.SetStatusUnsatisfiable("SpotAccess", + "The only possible exporters are under spot access (i.e. %s), but spot access is still not implemented", + orderedExporters[0].Exporter.Name) + return nil } - var policies jumpstarterdevv1alpha1.ExporterAccessPolicyList - if err := r.List(ctx, &policies, - client.InNamespace(lease.Namespace), - ); err != nil { - return fmt.Errorf("reconcileStatusExporterRef: failed to list exporter access policies: %w", err) + availableExporters := filterOutLeasedExporters(orderedExporters) + if len(availableExporters) == 0 { + lease.SetStatusPending("NotAvailable", + "There are %d approved exporters, but all of them are already leased", + len(approvedExporters)) + result.RequeueAfter = time.Second + return nil } - if len(policies.Items) == 0 { - for _, exporter := range availableExporters { - approvedExporters = append(approvedExporters, struct { - Exporter jumpstarterdevv1alpha1.Exporter - Policy jumpstarterdevv1alpha1.Policy - }{ - Exporter: exporter, - Policy: jumpstarterdevv1alpha1.Policy{ - Priority: 0, - SpotAccess: false, - }, - }) - } - } else { - var jclient jumpstarterdevv1alpha1.Client - if err := r.Get(ctx, types.NamespacedName{ - Namespace: lease.Namespace, - Name: lease.Spec.ClientRef.Name, - }, &jclient); err != nil { - return fmt.Errorf("reconcileStatusExporterRef: failed to get client: %w", err) - } + // TODO: here there's room for improvement, i.e. we could have multiple + // clients trying to lease the same exporters, we should look at priorities + // and spot access to decide which client gets the exporter, this probably means + // that we will need to construct a lease scheduler with the view of all leases + // and exporters in the system, and (maybe) a priority queue for the leases. - for _, exporter := range availableExporters { - for _, policy := range policies.Items { - exporterSelector, err := metav1.LabelSelectorAsSelector(&policy.Spec.ExporterSelector) - if err != nil { - return fmt.Errorf("reconcileStatusExporterRef: failed to convert exporter selector: %w", err) - } - if exporterSelector.Matches(labels.Set(exporter.Labels)) { - for _, p := range policy.Spec.Policies { - for _, from := range p.From { - clientSelector, err := metav1.LabelSelectorAsSelector(&from.ClientSelector) - if err != nil { - return fmt.Errorf("reconcileStatusExporterRef: failed to convert client selector: %w", err) - } - if clientSelector.Matches(labels.Set(jclient.Labels)) { - if p.MaximumDuration != nil { - if lease.Spec.Duration.Duration > p.MaximumDuration.Duration { - continue - } - } - approvedExporters = append(approvedExporters, struct { - Exporter jumpstarterdevv1alpha1.Exporter - Policy jumpstarterdevv1alpha1.Policy - }{ - Exporter: exporter, - Policy: p, - }) - } - } - } - } - } - } - } + // For now, we just select the best available exporter without considering other + // ongoing lease requests - if len(approvedExporters) == 0 { - meta.SetStatusCondition(&lease.Status.Conditions, metav1.Condition{ - Type: string(jumpstarterdevv1alpha1.LeaseConditionTypePending), - Status: metav1.ConditionTrue, - ObservedGeneration: lease.Generation, - LastTransitionTime: metav1.Time{ - Time: time.Now(), - }, - Reason: "NotAvailable", - }) + selected := availableExporters[0] + + if selected.ExistingLease != nil { + // TODO: Implement eviction of spot access leases + lease.SetStatusPending("NotAvailable", + "Exporter %s is already leased by another client under spot access, but spot access eviction still not implemented", + selected.Exporter.Name) result.RequeueAfter = time.Second return nil } - selected := approvedExporters[0] lease.Status.Priority = selected.Policy.Priority lease.Status.SpotAccess = selected.Policy.SpotAccess lease.Status.ExporterRef = &corev1.LocalObjectReference{ @@ -362,6 +284,201 @@ func (r *LeaseReconciler) reconcileStatusExporterRef( return nil } +// attachMatchingPolicies attaches the matching policies to the list of online exporters +// if the exporter matches the policy and the client matches the policy's client selector +// the exporter is approved for leasing +func (r *LeaseReconciler) attachMatchingPolicies(ctx context.Context, lease *jumpstarterdevv1alpha1.Lease, onlineExporters []jumpstarterdevv1alpha1.Exporter) ([]ApprovedExporter, error) { + var approvedExporters []ApprovedExporter + + var policies jumpstarterdevv1alpha1.ExporterAccessPolicyList + if err := r.List(ctx, &policies, + client.InNamespace(lease.Namespace), + ); err != nil { + return nil, fmt.Errorf("reconcileStatusExporterRef: failed to list exporter access policies: %w", err) + } + + // If there are no policies, we just approve all online exporters + if len(policies.Items) == 0 { + for _, exporter := range onlineExporters { + approvedExporters = append(approvedExporters, ApprovedExporter{ + Exporter: exporter, + Policy: jumpstarterdevv1alpha1.Policy{ + Priority: 0, + SpotAccess: false, + }, + }) + } + return approvedExporters, nil + } + // If policies exist: get the client to obtain the metadata necessary for policy matching + var jclient jumpstarterdevv1alpha1.Client + if err := r.Get(ctx, types.NamespacedName{ + Namespace: lease.Namespace, + Name: lease.Spec.ClientRef.Name, + }, &jclient); err != nil { + return nil, fmt.Errorf("reconcileStatusExporterRef: failed to get client: %w", err) + } + + for _, exporter := range onlineExporters { + for _, policy := range policies.Items { + exporterSelector, err := metav1.LabelSelectorAsSelector(&policy.Spec.ExporterSelector) + if err != nil { + return nil, fmt.Errorf("reconcileStatusExporterRef: failed to convert exporter selector: %w", err) + } + if exporterSelector.Matches(labels.Set(exporter.Labels)) { + for _, p := range policy.Spec.Policies { + for _, from := range p.From { + clientSelector, err := metav1.LabelSelectorAsSelector(&from.ClientSelector) + if err != nil { + return nil, fmt.Errorf("reconcileStatusExporterRef: failed to convert client selector: %w", err) + } + if clientSelector.Matches(labels.Set(jclient.Labels)) { + if p.MaximumDuration != nil { + if lease.Spec.Duration.Duration > p.MaximumDuration.Duration { + // TODO: we probably should keep this on the list of approved exporters + // but mark as excessive duration so we can report it on the status + // of lease if no other options exist + continue + } + } + approvedExporters = append(approvedExporters, ApprovedExporter{ + Exporter: exporter, + Policy: p, + }) + } + } + } + } + } + } + return approvedExporters, nil +} + +// ListMatchingExporters returns a list of exporters that match the selector of the lease +func (r *LeaseReconciler) ListMatchingExporters(ctx context.Context, lease *jumpstarterdevv1alpha1.Lease, + selector labels.Selector) (*jumpstarterdevv1alpha1.ExporterList, error) { + + var matchingExporters jumpstarterdevv1alpha1.ExporterList + if err := r.List( + ctx, + &matchingExporters, + client.InNamespace(lease.Namespace), + client.MatchingLabelsSelector{Selector: selector}, + ); err != nil { + return nil, fmt.Errorf("ListMatchingExporters: failed to list exporters matching selector: %w", err) + } + return &matchingExporters, nil +} + +// ListActiveLeases returns a list of active leases in the namespace +func (r *LeaseReconciler) ListActiveLeases(ctx context.Context, namespace string) (*jumpstarterdevv1alpha1.LeaseList, error) { + var activeLeases jumpstarterdevv1alpha1.LeaseList + if err := r.List( + ctx, + &activeLeases, + client.InNamespace(namespace), + MatchingActiveLeases(), + ); err != nil { + return nil, err + } + return &activeLeases, nil +} + +// attachExistingLeases attaches the existing leases to the approved exporter list +// if the activeLeases slice contains a lease that references the exporter in the +// approved exporter list +func attachExistingLeases(exporters []ApprovedExporter, activeLeases []jumpstarterdevv1alpha1.Lease) []ApprovedExporter { + for i, exporter := range exporters { + for _, existingLease := range activeLeases { + if existingLease.Status.ExporterRef != nil && + existingLease.Status.ExporterRef.Name == exporter.Exporter.Name { + exporters[i].ExistingLease = &existingLease + } + } + } + return exporters +} + +// orderAvailableExporters orders the exporters in the following order +// 1. Not being leased +// 2. Not accessible under spot access +// 3. Highest priority +// 4. Alphabetically by exporter name + +func orderApprovedExporters(exporters []ApprovedExporter) []ApprovedExporter { + // Order by lease status, priority, spot access, and name + + cmpFunc := func(a, b ApprovedExporter) int { + // If one of the exporters has an existing lease, we want to prioritize the one that doesn't + if a.ExistingLease != nil && b.ExistingLease == nil { + return 1 + } else if a.ExistingLease == nil && b.ExistingLease != nil { + return -1 + } + + // We want spot access policies to be later on the returned array + if a.Policy.SpotAccess != b.Policy.SpotAccess { + if a.Policy.SpotAccess { + return 1 + } + return -1 + } + + // We want the highest priority to be first + if a.Policy.Priority != b.Policy.Priority { + return b.Policy.Priority - a.Policy.Priority + } + + // If the priority is the same, we want to sort by exporter name + return strings.Compare(a.Exporter.Name, b.Exporter.Name) + } + + slices.SortFunc(exporters, cmpFunc) + + return exporters +} + +// filterOutLeasedExporters filters out the exporters that are already leased +func filterOutLeasedExporters(exporters []ApprovedExporter) []ApprovedExporter { + // Exclude exporter that are already leased and non-takeable + return slices.DeleteFunc(exporters, func(ae ApprovedExporter) bool { + existingLease := ae.ExistingLease + if existingLease == nil { + return false + } + + weHaveNonSpotAccess := !ae.Policy.SpotAccess + + // There is an existing lease, but, if it's spot access we can take it + if weHaveNonSpotAccess && ae.ExistingLease.Status.SpotAccess { + return false + } + + // ok, there is an existing lease, and it's not spot access, we can't take it + return true + }) + +} + +// filterOutOfflineExporters filters out the exporters that are not online +func filterOutOfflineExporters(matchingExporters []jumpstarterdevv1alpha1.Exporter) []jumpstarterdevv1alpha1.Exporter { + onlineExporters := slices.DeleteFunc( + matchingExporters, + func(exporter jumpstarterdevv1alpha1.Exporter) bool { + return !(true && + meta.IsStatusConditionTrue( + exporter.Status.Conditions, + string(jumpstarterdevv1alpha1.ExporterConditionTypeRegistered), + ) && + meta.IsStatusConditionTrue( + exporter.Status.Conditions, + string(jumpstarterdevv1alpha1.ExporterConditionTypeOnline), + )) + }, + ) + return onlineExporters +} + // SetupWithManager sets up the controller with the Manager. func (r *LeaseReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/internal/controller/lease_controller_test.go b/internal/controller/lease_controller_test.go index 1f4f7d0d..2d9a8233 100644 --- a/internal/controller/lease_controller_test.go +++ b/internal/controller/lease_controller_test.go @@ -63,6 +63,25 @@ var _ = Describe("Lease Controller", func() { deleteLeases(ctx, "lease1", "lease2", "lease3") }) + When("trying to lease with an empty selector", func() { + It("should fail right away", func() { + lease := leaseDutA2Sec.DeepCopy() + lease.Spec.Selector.MatchLabels = nil + + ctx := context.Background() + Expect(k8sClient.Create(ctx, lease)).To(Succeed()) + _ = reconcileLease(ctx, lease) + + updatedLease := getLease(ctx, lease.Name) + Expect(updatedLease.Status.ExporterRef).To(BeNil()) + + Expect(meta.IsStatusConditionTrue( + updatedLease.Status.Conditions, + string(jumpstarterdevv1alpha1.LeaseConditionTypeInvalid), + )).To(BeTrue()) + }) + }) + When("trying to lease an available exporter", func() { It("should acquire lease right away", func() { lease := leaseDutA2Sec.DeepCopy() @@ -391,3 +410,141 @@ func deleteLeases(ctx context.Context, leases ...string) { _ = k8sClient.Delete(ctx, leaseObj) } } + +var _ = Describe("orderApprovedExporters", func() { + When("approved exporters are under a lease", func() { + It("should put them last", func() { + approvedExporters := []ApprovedExporter{ + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 0, SpotAccess: false}, + Exporter: *testExporter1DutA, + ExistingLease: &jumpstarterdevv1alpha1.Lease{}, + }, + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 0, SpotAccess: false}, + Exporter: *testExporter2DutA, + }, + } + ordered := orderApprovedExporters(approvedExporters) + Expect(ordered[0].Exporter.Name).To(Equal(testExporter2DutA.Name)) + Expect(ordered[0].ExistingLease).To(BeNil()) + Expect(ordered[1].Exporter.Name).To(Equal(testExporter1DutA.Name)) + Expect(ordered[1].ExistingLease).NotTo(BeNil()) + }) + }) + + When("some approved exporters are accessible in spot mode", func() { + It("should put them last", func() { + approvedExporters := []ApprovedExporter{ + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 0, SpotAccess: true}, + Exporter: *testExporter1DutA, + ExistingLease: &jumpstarterdevv1alpha1.Lease{}, + }, + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 0, SpotAccess: false}, + Exporter: *testExporter2DutA, + ExistingLease: &jumpstarterdevv1alpha1.Lease{}, + }, + } + ordered := orderApprovedExporters(approvedExporters) + Expect(ordered[0].Exporter.Name).To(Equal(testExporter2DutA.Name)) + Expect(ordered[0].Policy.SpotAccess).To(BeFalse()) + Expect(ordered[1].Exporter.Name).To(Equal(testExporter1DutA.Name)) + Expect(ordered[1].Policy.SpotAccess).To(BeTrue()) + }) + }) + + When("some approved exporters have different policy priorities", func() { + It("should order them by priority", func() { + approvedExporters := []ApprovedExporter{ + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 5, SpotAccess: false}, + Exporter: *testExporter1DutA, + }, + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 10, SpotAccess: false}, + Exporter: *testExporter2DutA, + }, + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 100, SpotAccess: false}, + Exporter: *testExporter2DutA, + }, + } + ordered := orderApprovedExporters(approvedExporters) + Expect(ordered[0].Policy.Priority).To(Equal(int(100))) + Expect(ordered[1].Policy.Priority).To(Equal(int(10))) + Expect(ordered[2].Policy.Priority).To(Equal(int(5))) + + }) + }) + + When("some approved exporters have same policy priorities and no other traits", func() { + It("should order them by name", func() { + approvedExporters := []ApprovedExporter{ + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 5, SpotAccess: false}, + Exporter: *testExporter2DutA, + }, + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 5, SpotAccess: false}, + Exporter: *testExporter1DutA, + }, + } + ordered := orderApprovedExporters(approvedExporters) + + Expect(ordered[0].Exporter.Name).To(Equal(testExporter1DutA.Name)) + Expect(ordered[1].Exporter.Name).To(Equal(testExporter2DutA.Name)) + }) + }) + + When("mixed priorities, spot access, lease status are in the list", func() { + It("should order them properly", func() { + approvedExporters := []ApprovedExporter{ + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 5, SpotAccess: false}, + Exporter: *testExporter2DutA, + }, + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 100, SpotAccess: true}, + Exporter: *testExporter2DutA, + ExistingLease: &jumpstarterdevv1alpha1.Lease{}, + }, + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 10, SpotAccess: false}, + Exporter: *testExporter1DutA, + }, + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 5, SpotAccess: false}, + Exporter: *testExporter1DutA, + }, + { + Policy: jumpstarterdevv1alpha1.Policy{Priority: 10, SpotAccess: true}, + Exporter: *testExporter2DutA, + }, + } + + ordered := orderApprovedExporters(approvedExporters) + Expect(ordered[0].Policy.Priority).To(Equal(int(10))) + Expect(ordered[0].Policy.SpotAccess).To(BeFalse()) + Expect(ordered[0].Exporter.Name).To(Equal(testExporter1DutA.Name)) + + Expect(ordered[1].Policy.Priority).To(Equal(int(5))) + Expect(ordered[1].Policy.SpotAccess).To(BeFalse()) + Expect(ordered[1].Exporter.Name).To(Equal(testExporter1DutA.Name)) + + Expect(ordered[2].Policy.Priority).To(Equal(int(5))) + Expect(ordered[2].Policy.SpotAccess).To(BeFalse()) + Expect(ordered[2].Exporter.Name).To(Equal(testExporter2DutA.Name)) + + Expect(ordered[3].Policy.Priority).To(Equal(int(10))) + Expect(ordered[3].Policy.SpotAccess).To(BeTrue()) + Expect(ordered[3].Exporter.Name).To(Equal(testExporter2DutA.Name)) + + Expect(ordered[4].Policy.Priority).To(Equal(int(100))) + Expect(ordered[4].Policy.SpotAccess).To(BeTrue()) + Expect(ordered[4].Exporter.Name).To(Equal(testExporter2DutA.Name)) + + }) + }) +})