Skip to content
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
9 changes: 9 additions & 0 deletions controller/api/v1alpha1/lease_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ func LeaseFromProtobuf(
ClientRef: clientRef,
Duration: duration,
Selector: *selector,
ExporterRef: func() *corev1.LocalObjectReference {
if req.ExporterName == nil || *req.ExporterName == "" {
return nil
}
return &corev1.LocalObjectReference{Name: *req.ExporterName}
}(),
BeginTime: beginTime,
EndTime: endTime,
},
Expand Down Expand Up @@ -233,6 +239,9 @@ func (l *Lease) ToProtobuf() *cpb.Lease {
Client: ptr.To(fmt.Sprintf("namespaces/%s/clients/%s", l.Namespace, l.Spec.ClientRef.Name)),
Conditions: conditions,
}
if l.Spec.ExporterRef != nil {
lease.ExporterName = ptr.To(l.Spec.ExporterRef.Name)
}
if l.Spec.Duration != nil {
lease.Duration = durationpb.New(l.Spec.Duration.Duration)
}
Expand Down
18 changes: 18 additions & 0 deletions controller/api/v1alpha1/lease_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,5 +313,23 @@ var _ = Describe("LeaseFromProtobuf", func() {
Expect(lease).NotTo(BeNil())
Expect(lease.Labels).To(BeEmpty()) // nil or empty map is fine
})

It("should keep requested exporter name in spec exporterRef", func() {
exporterName := "device-1"
pbLease := &cpb.Lease{
Selector: "",
Duration: durationpb.New(time.Hour),
ExporterName: &exporterName,
}
key := types.NamespacedName{Name: "test-lease", Namespace: "default"}
clientRef := corev1.LocalObjectReference{Name: "test-client"}

lease, err := LeaseFromProtobuf(pbLease, key, clientRef)

Expect(err).NotTo(HaveOccurred())
Expect(lease).NotTo(BeNil())
Expect(lease.Spec.ExporterRef).NotTo(BeNil())
Expect(lease.Spec.ExporterRef.Name).To(Equal(exporterName))
})
})
})
4 changes: 4 additions & 0 deletions controller/api/v1alpha1/lease_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// +kubebuilder:validation:XValidation:rule="((has(self.selector.matchLabels) && size(self.selector.matchLabels) > 0) || (has(self.selector.matchExpressions) && size(self.selector.matchExpressions) > 0)) || (has(self.exporterRef) && has(self.exporterRef.name) && size(self.exporterRef.name) > 0)",message="one of selector or exporterRef.name is required"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fancy! :D

// LeaseSpec defines the desired state of Lease
type LeaseSpec struct {
// The client that is requesting the lease
Expand All @@ -30,7 +31,10 @@ type LeaseSpec struct {
// in which case it's calculated as EndTime - BeginTime.
Duration *metav1.Duration `json:"duration,omitempty"`
// The selector for the exporter to be used
// +kubebuilder:default:={}
Selector metav1.LabelSelector `json:"selector"`
// Optionally pin this lease to a specific exporter name.
ExporterRef *corev1.LocalObjectReference `json:"exporterRef,omitempty"`
// The release flag requests the controller to end the lease now
Release bool `json:"release,omitempty"`
// Requested start time. If omitted, lease starts when exporter is acquired.
Expand Down
5 changes: 5 additions & 0 deletions controller/api/v1alpha1/zz_generated.deepcopy.go

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

Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,26 @@ spec:
Can be updated to extend or shorten active leases.
format: date-time
type: string
exporterRef:
description: Optionally pin this lease to a specific exporter name.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
release:
description: The release flag requests the controller to end the lease
now
type: boolean
selector:
default: {}
description: The selector for the exporter to be used
properties:
matchExpressions:
Expand Down Expand Up @@ -135,6 +150,12 @@ spec:
- clientRef
- selector
type: object
x-kubernetes-validations:
- message: one of selector or exporterRef.name is required
rule: ((has(self.selector.matchLabels) && size(self.selector.matchLabels)
> 0) || (has(self.selector.matchExpressions) && size(self.selector.matchExpressions)
> 0)) || (has(self.exporterRef) && has(self.exporterRef.name) && size(self.exporterRef.name)
> 0)
status:
description: LeaseStatus defines the observed state of Lease
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,26 @@ spec:
Can be updated to extend or shorten active leases.
format: date-time
type: string
exporterRef:
description: Optionally pin this lease to a specific exporter name.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
Comment thread
coderabbitai[bot] marked this conversation as resolved.
release:
description: The release flag requests the controller to end the lease
now
type: boolean
selector:
default: {}
description: The selector for the exporter to be used
properties:
matchExpressions:
Expand Down Expand Up @@ -135,6 +150,12 @@ spec:
- clientRef
- selector
type: object
x-kubernetes-validations:
- message: one of selector or exporterRef.name is required
rule: ((has(self.selector.matchLabels) && size(self.selector.matchLabels)
> 0) || (has(self.selector.matchExpressions) && size(self.selector.matchExpressions)
> 0)) || (has(self.exporterRef) && has(self.exporterRef.name) && size(self.exporterRef.name)
> 0)
status:
description: LeaseStatus defines the observed state of Lease
properties:
Expand Down
45 changes: 38 additions & 7 deletions controller/internal/controller/lease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

jumpstarterdevv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
Expand Down Expand Up @@ -214,18 +215,48 @@ func (r *LeaseReconciler) reconcileStatusExporterRef(
selector, err := lease.GetExporterSelector()
if err != nil {
return fmt.Errorf("reconcileStatusExporterRef: failed to get exporter selector: %w", err)
} else if selector.Empty() {
} else if selector.Empty() && lease.Spec.ExporterRef == nil {
lease.SetStatusInvalid("InvalidSelector", "The selector for the lease is empty, a selector is required")
return nil
}

// List all Exporter matching selector
matchingExporters, err := r.ListMatchingExporters(ctx, lease, selector)
if err != nil {
return fmt.Errorf("reconcileStatusExporterRef: failed to list matching exporters: %w", err)
var matchingExporters []jumpstarterdevv1alpha1.Exporter
if lease.Spec.ExporterRef != nil {
var exporter jumpstarterdevv1alpha1.Exporter
if err := r.Get(ctx, types.NamespacedName{
Namespace: lease.Namespace,
Name: lease.Spec.ExporterRef.Name,
}, &exporter); err != nil {
if k8serrors.IsNotFound(err) {
lease.SetStatusUnsatisfiable(
"ExporterNotFound",
"Requested exporter %s was not found",
lease.Spec.ExporterRef.Name,
)
return nil
}
return fmt.Errorf("reconcileStatusExporterRef: failed to get requested exporter: %w", err)
}
if !selector.Empty() && !selector.Matches(labels.Set(exporter.Labels)) {
lease.SetStatusUnsatisfiable(
"SelectorMismatch",
"Requested exporter %s does not match selector %s",
exporter.Name,
metav1.FormatLabelSelector(&lease.Spec.Selector),
)
return nil
}
matchingExporters = []jumpstarterdevv1alpha1.Exporter{exporter}
} else {
// List all exporters matching selector
listed, err := r.ListMatchingExporters(ctx, lease, selector)
if err != nil {
return fmt.Errorf("reconcileStatusExporterRef: failed to list matching exporters: %w", err)
}
matchingExporters = listed.Items
}

approvedExporters, err := r.attachMatchingPolicies(ctx, lease, matchingExporters.Items)
approvedExporters, err := r.attachMatchingPolicies(ctx, lease, matchingExporters)
if err != nil {
return fmt.Errorf("reconcileStatusExporterRef: failed to handle policy approval: %w", err)
}
Expand All @@ -234,7 +265,7 @@ func (r *LeaseReconciler) reconcileStatusExporterRef(
lease.SetStatusUnsatisfiable(
"NoAccess",
"While there are %d exporters matching the selector, none of them are approved by any policy for your client",
len(matchingExporters.Items),
len(matchingExporters),
)
return nil
}
Expand Down
65 changes: 62 additions & 3 deletions controller/internal/controller/lease_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,79 @@ var _ = Describe("Lease Controller", func() {
lease := leaseDutA2Sec.DeepCopy()
lease.Spec.Selector.MatchLabels = nil

ctx := context.Background()
err := k8sClient.Create(ctx, lease)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("one of selector or exporterRef.name is required"))
})
})

When("trying to lease with a requested exporter and empty selector", func() {
It("should acquire the requested exporter", func() {
lease := leaseDutA2Sec.DeepCopy()
lease.Spec.Selector.MatchLabels = nil
lease.Spec.ExporterRef = &corev1.LocalObjectReference{Name: testExporter1DutA.Name}

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(updatedLease.Status.ExporterRef).NotTo(BeNil())
Expect(updatedLease.Status.ExporterRef.Name).To(Equal(testExporter1DutA.Name))
Expect(meta.IsStatusConditionTrue(
updatedLease.Status.Conditions,
string(jumpstarterdevv1alpha1.LeaseConditionTypeInvalid),
string(jumpstarterdevv1alpha1.LeaseConditionTypeReady),
)).To(BeTrue())
})
})

When("trying to lease with a missing requested exporter", func() {
It("should be unsatisfiable with ExporterNotFound reason", func() {
lease := leaseDutA2Sec.DeepCopy()
lease.Spec.Selector.MatchLabels = nil
lease.Spec.ExporterRef = &corev1.LocalObjectReference{Name: "does-not-exist"}

ctx := context.Background()
Expect(k8sClient.Create(ctx, lease)).To(Succeed())
_ = reconcileLease(ctx, lease)

updatedLease := getLease(ctx, lease.Name)
Expect(updatedLease.Status.ExporterRef).To(BeNil())
condition := meta.FindStatusCondition(
updatedLease.Status.Conditions,
string(jumpstarterdevv1alpha1.LeaseConditionTypeUnsatisfiable),
)
Expect(condition).NotTo(BeNil())
Expect(condition.Reason).To(Equal("ExporterNotFound"))
Expect(meta.IsStatusConditionTrue(
updatedLease.Status.Conditions,
string(jumpstarterdevv1alpha1.LeaseConditionTypePending),
)).To(BeFalse())
})
})

When("trying to lease with requested exporter that does not match selector", func() {
It("should fail with SelectorMismatch reason", func() {
lease := leaseDutA2Sec.DeepCopy()
lease.Spec.Selector.MatchLabels["dut"] = "b"
lease.Spec.ExporterRef = &corev1.LocalObjectReference{Name: testExporter1DutA.Name}

ctx := context.Background()
Expect(k8sClient.Create(ctx, lease)).To(Succeed())
_ = reconcileLease(ctx, lease)

updatedLease := getLease(ctx, lease.Name)
Expect(updatedLease.Status.ExporterRef).To(BeNil())
condition := meta.FindStatusCondition(
updatedLease.Status.Conditions,
string(jumpstarterdevv1alpha1.LeaseConditionTypeUnsatisfiable),
)
Expect(condition).NotTo(BeNil())
Expect(condition.Reason).To(Equal("SelectorMismatch"))
})
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

When("trying to lease an available exporter", func() {
It("should acquire lease right away", func() {
lease := leaseDutA2Sec.DeepCopy()
Expand Down
16 changes: 13 additions & 3 deletions controller/internal/protocol/jumpstarter/client/v1/client.pb.go

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

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

Loading
Loading