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
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,15 @@ spec:
type: object
machineName:
type: string
token:
description: |-
Token is an optional static token for the registration URL.
When set, the registration URL will be deterministic: {server-url}/elemental/registration/{token}.
Must be URL-safe: lowercase alphanumeric characters and hyphens only.
When empty, a random token is generated (default behavior).
maxLength: 253
pattern: ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$
type: string
type: object
status:
properties:
Expand Down
3 changes: 3 additions & 0 deletions api/v1beta1/condition_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const (

// RbacCreationFailureReason documents a machine registration object that has RBAC creation failures.
RbacCreationFailureReason = "RbacCreationFailure"

// DuplicateTokenReason documents that another machine registration already uses the same token.
DuplicateTokenReason = "DuplicateToken"
)

// Machine Inventory conditions
Expand Down
8 changes: 8 additions & 0 deletions api/v1beta1/machineregistration_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ const (
)

type MachineRegistrationSpec struct {
// Token is an optional static token for the registration URL.
// When set, the registration URL will be deterministic: {server-url}/elemental/registration/{token}.
// Must be URL-safe: lowercase alphanumeric characters and hyphens only.
// When empty, a random token is generated (default behavior).
// +optional
// +kubebuilder:validation:Pattern=`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`
// +kubebuilder:validation:MaxLength=253
Token string `json:"token,omitempty"`
// +optional
MachineName string `json:"machineName,omitempty"`
// MachineInventoryLabels label to be added to the created MachineInventory object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ spec:
type: object
machineName:
type: string
token:
description: |-
Token is an optional static token for the registration URL.
When set, the registration URL will be deterministic: {server-url}/elemental/registration/{token}.
Must be URL-safe: lowercase alphanumeric characters and hyphens only.
When empty, a random token is generated (default behavior).
maxLength: 253
pattern: ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$
type: string
type: object
status:
properties:
Expand Down
46 changes: 42 additions & 4 deletions controllers/machineregistration_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import (
"github.com/rancher/elemental-operator/pkg/util"
)

var errDuplicateToken = errors.New("duplicate token")

// MachineRegistrationReconciler reconciles a MachineRegistration object.
type MachineRegistrationReconciler struct {
client.Client
Expand Down Expand Up @@ -126,10 +128,14 @@ func (r *MachineRegistrationReconciler) reconcile(ctx context.Context, mRegistra
}

if err := r.setRegistrationTokenAndURL(ctx, mRegistration); err != nil {
reason := elementalv1.MissingTokenOrServerURLReason
if errors.Is(err, errDuplicateToken) {
reason = elementalv1.DuplicateTokenReason
}
meta.SetStatusCondition(&mRegistration.Status.Conditions, metav1.Condition{
Type: elementalv1.ReadyCondition,
Status: metav1.ConditionFalse,
Reason: elementalv1.MissingTokenOrServerURLReason,
Reason: reason,
Message: err.Error(),
})
return ctrl.Result{}, fmt.Errorf("failed to set registration token and url: %w", err)
Expand All @@ -156,6 +162,12 @@ func (r *MachineRegistrationReconciler) reconcile(ctx context.Context, mRegistra

func (r *MachineRegistrationReconciler) isReady(ctx context.Context, mRegistration *elementalv1.MachineRegistration) bool {
if meta.IsStatusConditionTrue(mRegistration.Status.Conditions, elementalv1.ReadyCondition) {
// If the spec defines a static endpoint but the status token doesn't match, re-reconcile.
if mRegistration.Spec.Token != "" && mRegistration.Status.RegistrationToken != mRegistration.Spec.Token {
mRegistration.Status.RegistrationToken = ""
mRegistration.Status.RegistrationURL = ""
return false
}
// Despite being on ready state we check if the serviceaccount token is still available as it can be deleted
// by the control plane during backup & restore operations see: rancher/elemental#776
if err := r.Get(ctx, types.NamespacedName{
Expand All @@ -178,9 +190,16 @@ func (r *MachineRegistrationReconciler) setRegistrationTokenAndURL(ctx context.C
logger.Info("Setting registration token and url")

if mRegistration.Status.RegistrationToken == "" {
mRegistration.Status.RegistrationToken, err = randomtoken.Generate()
if err != nil {
return fmt.Errorf("failed to generate registration token: %w", err)
if mRegistration.Spec.Token != "" {
if err := r.checkTokenUniqueness(ctx, mRegistration); err != nil {
return err
}
mRegistration.Status.RegistrationToken = mRegistration.Spec.Token
} else {
mRegistration.Status.RegistrationToken, err = randomtoken.Generate()
if err != nil {
return fmt.Errorf("failed to generate registration token: %w", err)
}
}
}

Expand All @@ -195,6 +214,25 @@ func (r *MachineRegistrationReconciler) setRegistrationTokenAndURL(ctx context.C
return nil
}

func (r *MachineRegistrationReconciler) checkTokenUniqueness(ctx context.Context, mRegistration *elementalv1.MachineRegistration) error {
mRegistrationList := &elementalv1.MachineRegistrationList{}
if err := r.List(ctx, mRegistrationList); err != nil {
return fmt.Errorf("failed to list machine registrations: %w", err)
}

for _, m := range mRegistrationList.Items {
if m.UID == mRegistration.UID {
continue
}
if m.Status.RegistrationToken == mRegistration.Spec.Token {
return fmt.Errorf("token %q is already in use by %s/%s: %w",
mRegistration.Spec.Token, m.Namespace, m.Name, errDuplicateToken)
}
}

return nil
}

func (r *MachineRegistrationReconciler) getRancherServerURL(ctx context.Context) (string, error) {
logger := ctrl.LoggerFrom(ctx)

Expand Down
54 changes: 54 additions & 0 deletions controllers/machineregistration_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,60 @@ var _ = Describe("setRegistrationTokenAndURL", func() {
Expect(err.Error()).To(ContainSubstring("server-url is not set"))
Expect(test.CleanupAndWait(ctx, cl, setting)).To(Succeed())
})

It("should use static registration endpoint when set", func() {
setting := &managementv3.Setting{
ObjectMeta: metav1.ObjectMeta{
Name: "server-url",
},
Value: "https://example.com",
}
Expect(cl.Create(ctx, setting)).To(Succeed())
mRegistration.Spec.Token = "my-static-endpoint"
Expect(r.setRegistrationTokenAndURL(ctx, mRegistration)).To(Succeed())
Expect(mRegistration.Status.RegistrationToken).To(Equal("my-static-endpoint"))
Expect(mRegistration.Status.RegistrationURL).To(Equal("https://example.com/elemental/registration/my-static-endpoint"))
Expect(test.CleanupAndWait(ctx, cl, setting)).To(Succeed())
})

It("should return error when static registration endpoint is already in use", func() {
setting := &managementv3.Setting{
ObjectMeta: metav1.ObjectMeta{
Name: "server-url",
},
Value: "https://example.com",
}
Expect(cl.Create(ctx, setting)).To(Succeed())

// Create and reconcile the first registration with a static endpoint
mRegistration.Spec.Token = "shared-endpoint"
Expect(cl.Create(ctx, mRegistration)).To(Succeed())
_, err := r.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: mRegistration.Namespace,
Name: mRegistration.Name,
},
})
Expect(err).ToNot(HaveOccurred())

// Create a second registration with the same static endpoint
mRegistration2 := &elementalv1.MachineRegistration{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name-2",
Namespace: "default",
},
Spec: elementalv1.MachineRegistrationSpec{
Token: "shared-endpoint",
},
}
Expect(cl.Create(ctx, mRegistration2)).To(Succeed())

err = r.setRegistrationTokenAndURL(ctx, mRegistration2)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("already in use"))

Expect(test.CleanupAndWait(ctx, cl, setting, mRegistration2)).To(Succeed())
})
})

var _ = Describe("createRBACObjects", func() {
Expand Down
Loading