From c8803af8c953de14ac7da1e1749784bc6dd14d3e Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Mon, 19 Jan 2026 15:23:45 -0800 Subject: [PATCH 1/7] feat: add connector and advertisement controllers --- api/v1alpha1/connector_types.go | 15 ++ api/v1alpha1/connectoradvertisement_types.go | 22 ++- api/v1alpha1/zz_generated.deepcopy.go | 15 +- cmd/main.go | 9 + ...datumapis.com_connectoradvertisements.yaml | 68 +++++++ .../networking.datumapis.com_connectors.yaml | 7 + config/rbac/role.yaml | 14 ++ internal/controller/connector_controller.go | 106 +++++++++++ .../controller/connector_controllers_test.go | 172 ++++++++++++++++++ .../connectoradvertisement_controller.go | 117 ++++++++++++ 10 files changed, 538 insertions(+), 7 deletions(-) create mode 100644 internal/controller/connector_controller.go create mode 100644 internal/controller/connector_controllers_test.go create mode 100644 internal/controller/connectoradvertisement_controller.go diff --git a/api/v1alpha1/connector_types.go b/api/v1alpha1/connector_types.go index 3869c9ad..af2bf4ad 100644 --- a/api/v1alpha1/connector_types.go +++ b/api/v1alpha1/connector_types.go @@ -138,6 +138,19 @@ type ConnectorStatus struct { ConnectionDetails *ConnectorConnectionDetails `json:"connectionDetails,omitempty"` } +const ( + // ConnectorConditionAccepted indicates whether the ConnectorClass is resolved. + ConnectorConditionAccepted = "Accepted" + // ConnectorReasonAccepted indicates the Connector is accepted by the controller. + ConnectorReasonAccepted = "Accepted" + // ConnectorReasonPending indicates the Connector has not been processed yet. + ConnectorReasonPending = "Pending" + // ConnectorReasonConnectorClassNotFound indicates the referenced class is missing. + ConnectorReasonConnectorClassNotFound = "ConnectorClassNotFound" + // ConnectorReasonConnectorClassNotSpecified indicates the class name is empty. + ConnectorReasonConnectorClassNotSpecified = "ConnectorClassNotSpecified" +) + const ConnectorNameAnnotation = "networking.datum.org/connector-name" // +kubebuilder:object:root=true @@ -154,6 +167,8 @@ type Connector struct { Spec ConnectorSpec `json:"spec,omitempty"` // Status defines the observed state of a Connector + // + // +kubebuilder:default={conditions: {{type: "Accepted", status: "Unknown", reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"}}} Status ConnectorStatus `json:"status,omitempty"` } diff --git a/api/v1alpha1/connectoradvertisement_types.go b/api/v1alpha1/connectoradvertisement_types.go index e5e8b00a..74d1c505 100644 --- a/api/v1alpha1/connectoradvertisement_types.go +++ b/api/v1alpha1/connectoradvertisement_types.go @@ -84,7 +84,7 @@ type ConnectorAdvertisementSpec struct { // ConnectorRef references the Connector being advertised. // // +kubebuilder:validation:Required - ConnectorRef *LocalConnectorReference `json:"connectorRef"` + ConnectorRef LocalConnectorReference `json:"connectorRef"` // Layer 4 services being advertised. // @@ -97,8 +97,26 @@ type ConnectorAdvertisementSpec struct { // ConnectorAdvertisementStatus defines the observed state of ConnectorAdvertisement. type ConnectorAdvertisementStatus struct { + // Conditions describe the current conditions of the ConnectorAdvertisement. + // + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty"` } +const ( + // ConnectorAdvertisementConditionAccepted indicates the connector reference is resolved. + ConnectorAdvertisementConditionAccepted = "Accepted" + // ConnectorAdvertisementReasonAccepted indicates the advertisement is accepted. + ConnectorAdvertisementReasonAccepted = "Accepted" + // ConnectorAdvertisementReasonPending indicates the advertisement has not been processed yet. + ConnectorAdvertisementReasonPending = "Pending" + // ConnectorAdvertisementReasonConnectorNotFound indicates the referenced connector is missing. + ConnectorAdvertisementReasonConnectorNotFound = "ConnectorNotFound" + // ConnectorAdvertisementReasonConnectorRefMissing indicates the connectorRef is missing or empty. + ConnectorAdvertisementReasonConnectorRefMissing = "ConnectorRefMissing" +) + // +kubebuilder:object:root=true // +kubebuilder:subresource:status @@ -113,6 +131,8 @@ type ConnectorAdvertisement struct { Spec ConnectorAdvertisementSpec `json:"spec,omitempty"` // Status defines the observed state of a ConnectorAdvertisement + // + // +kubebuilder:default={conditions: {{type: "Accepted", status: "Unknown", reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"}}} Status ConnectorAdvertisementStatus `json:"status,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8b033989..28dd6d39 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -44,7 +44,7 @@ func (in *ConnectorAdvertisement) DeepCopyInto(out *ConnectorAdvertisement) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectorAdvertisement. @@ -142,11 +142,7 @@ func (in *ConnectorAdvertisementList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConnectorAdvertisementSpec) DeepCopyInto(out *ConnectorAdvertisementSpec) { *out = *in - if in.ConnectorRef != nil { - in, out := &in.ConnectorRef, &out.ConnectorRef - *out = new(LocalConnectorReference) - **out = **in - } + out.ConnectorRef = in.ConnectorRef if in.Layer4 != nil { in, out := &in.Layer4, &out.Layer4 *out = make([]ConnectorAdvertisementLayer4, len(*in)) @@ -169,6 +165,13 @@ func (in *ConnectorAdvertisementSpec) DeepCopy() *ConnectorAdvertisementSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConnectorAdvertisementStatus) DeepCopyInto(out *ConnectorAdvertisementStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectorAdvertisementStatus. diff --git a/cmd/main.go b/cmd/main.go index a2e9e57e..76fcc891 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -280,6 +280,15 @@ func main() { os.Exit(1) } + if err := (&controller.ConnectorReconciler{}).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Connector") + os.Exit(1) + } + if err := (&controller.ConnectorAdvertisementReconciler{}).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ConnectorAdvertisement") + os.Exit(1) + } + if err := controller.AddIndexers(ctx, mgr); err != nil { setupLog.Error(err, "unable to add indexers") os.Exit(1) diff --git a/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml b/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml index 2bc646bc..b3b5fb2a 100644 --- a/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml +++ b/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml @@ -123,7 +123,75 @@ spec: - connectorRef type: object status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted description: Status defines the observed state of a ConnectorAdvertisement + properties: + conditions: + description: Conditions describe the current conditions of the ConnectorAdvertisement. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map type: object required: - spec diff --git a/config/crd/bases/networking.datumapis.com_connectors.yaml b/config/crd/bases/networking.datumapis.com_connectors.yaml index fffa55db..7d578dc9 100644 --- a/config/crd/bases/networking.datumapis.com_connectors.yaml +++ b/config/crd/bases/networking.datumapis.com_connectors.yaml @@ -71,6 +71,13 @@ spec: type: string type: object status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted description: Status defines the observed state of a Connector properties: capabilities: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 8c6f5326..7473da08 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -136,6 +136,8 @@ rules: - apiGroups: - networking.datumapis.com resources: + - connectoradvertisements + - connectors - domains - httpproxies - networkbindings @@ -156,6 +158,8 @@ rules: - apiGroups: - networking.datumapis.com resources: + - connectoradvertisements/finalizers + - connectors/finalizers - domains/finalizers - httpproxies/finalizers - networkbindings/finalizers @@ -170,6 +174,8 @@ rules: - apiGroups: - networking.datumapis.com resources: + - connectoradvertisements/status + - connectors/status - domains/status - httpproxies/status - networkbindings/status @@ -183,3 +189,11 @@ rules: - get - patch - update +- apiGroups: + - networking.datumapis.com + resources: + - connectorclasses + verbs: + - get + - list + - watch diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go new file mode 100644 index 00000000..aab492b0 --- /dev/null +++ b/internal/controller/connector_controller.go @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package controller + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + networkingv1alpha1 "go.datum.net/network-services-operator/api/v1alpha1" +) + +// ConnectorReconciler reconciles a Connector object +type ConnectorReconciler struct { + mgr mcmanager.Manager +} + +// +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectors,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectors/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectors/finalizers,verbs=update +// +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectorclasses,verbs=get;list;watch + +func (r *ConnectorReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (_ ctrl.Result, err error) { + logger := log.FromContext(ctx, "cluster", req.ClusterName) + ctx = log.IntoContext(ctx, logger) + + cl, err := r.mgr.GetCluster(ctx, req.ClusterName) + if err != nil { + return ctrl.Result{}, err + } + + var connector networkingv1alpha1.Connector + if err := cl.GetClient().Get(ctx, req.NamespacedName, &connector); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + if !connector.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + logger.Info("reconciling connector") + defer logger.Info("reconcile complete") + + originalStatus := connector.Status.DeepCopy() + + acceptedCondition := apimeta.FindStatusCondition(connector.Status.Conditions, networkingv1alpha1.ConnectorConditionAccepted) + if acceptedCondition == nil { + acceptedCondition = &metav1.Condition{ + Type: networkingv1alpha1.ConnectorConditionAccepted, + } + } + acceptedCondition.ObservedGeneration = connector.Generation + + if connector.Spec.ConnectorClassName == "" { + acceptedCondition.Status = metav1.ConditionFalse + acceptedCondition.Reason = networkingv1alpha1.ConnectorReasonConnectorClassNotSpecified + acceptedCondition.Message = "connectorClassName is required" + } else { + var connectorClass networkingv1alpha1.ConnectorClass + if err := cl.GetClient().Get(ctx, client.ObjectKey{Name: connector.Spec.ConnectorClassName}, &connectorClass); err != nil { + if apierrors.IsNotFound(err) { + acceptedCondition.Status = metav1.ConditionFalse + acceptedCondition.Reason = networkingv1alpha1.ConnectorReasonConnectorClassNotFound + acceptedCondition.Message = fmt.Sprintf("ConnectorClass %q not found", connector.Spec.ConnectorClassName) + } else { + return ctrl.Result{}, err + } + } else { + acceptedCondition.Status = metav1.ConditionTrue + acceptedCondition.Reason = networkingv1alpha1.ConnectorReasonAccepted + acceptedCondition.Message = "Connector class resolved" + } + } + + apimeta.SetStatusCondition(&connector.Status.Conditions, *acceptedCondition) + + if !equality.Semantic.DeepEqual(*originalStatus, connector.Status) { + if statusErr := cl.GetClient().Status().Update(ctx, &connector); statusErr != nil { + return ctrl.Result{}, fmt.Errorf("failed updating connector status: %w", statusErr) + } + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ConnectorReconciler) SetupWithManager(mgr mcmanager.Manager) error { + r.mgr = mgr + return mcbuilder.ControllerManagedBy(mgr). + For(&networkingv1alpha1.Connector{}). + Named("connector"). + Complete(r) +} diff --git a/internal/controller/connector_controllers_test.go b/internal/controller/connector_controllers_test.go new file mode 100644 index 00000000..8da7d516 --- /dev/null +++ b/internal/controller/connector_controllers_test.go @@ -0,0 +1,172 @@ +package controller + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + networkingv1alpha1 "go.datum.net/network-services-operator/api/v1alpha1" +) + +func TestConnectorReconcile(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + tests := []struct { + name string + connector *networkingv1alpha1.Connector + connectorClass *networkingv1alpha1.ConnectorClass + wantStatus metav1.ConditionStatus + wantReason string + }{ + { + name: "connector class resolved", + connector: &networkingv1alpha1.Connector{ + ObjectMeta: metav1.ObjectMeta{Name: "connector", Namespace: "default"}, + Spec: networkingv1alpha1.ConnectorSpec{ + ConnectorClassName: "datum-connect", + }, + }, + connectorClass: &networkingv1alpha1.ConnectorClass{ + ObjectMeta: metav1.ObjectMeta{Name: "datum-connect"}, + }, + wantStatus: metav1.ConditionTrue, + wantReason: networkingv1alpha1.ConnectorReasonAccepted, + }, + { + name: "connector class missing", + connector: &networkingv1alpha1.Connector{ + ObjectMeta: metav1.ObjectMeta{Name: "connector", Namespace: "default"}, + Spec: networkingv1alpha1.ConnectorSpec{ + ConnectorClassName: "missing", + }, + }, + wantStatus: metav1.ConditionFalse, + wantReason: networkingv1alpha1.ConnectorReasonConnectorClassNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testScheme := runtime.NewScheme() + assert.NoError(t, scheme.AddToScheme(testScheme)) + assert.NoError(t, networkingv1alpha1.AddToScheme(testScheme)) + + builder := fake.NewClientBuilder().WithScheme(testScheme).WithObjects(tt.connector) + if tt.connectorClass != nil { + builder = builder.WithObjects(tt.connectorClass) + } + builder = builder.WithStatusSubresource(tt.connector) + cl := builder.Build() + + reconciler := &ConnectorReconciler{mgr: &fakeMockManager{cl: cl}} + req := mcreconcile.Request{ + Request: reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(tt.connector), + }, + ClusterName: "single", + } + + _, err := reconciler.Reconcile(context.Background(), req) + assert.NoError(t, err) + + var updated networkingv1alpha1.Connector + assert.NoError(t, cl.Get(context.Background(), client.ObjectKeyFromObject(tt.connector), &updated)) + + condition := apimeta.FindStatusCondition(updated.Status.Conditions, networkingv1alpha1.ConnectorConditionAccepted) + if assert.NotNil(t, condition) { + assert.Equal(t, tt.wantStatus, condition.Status) + assert.Equal(t, tt.wantReason, condition.Reason) + } + }) + } +} + +func TestConnectorAdvertisementReconcile(t *testing.T) { + log.SetLogger(zap.New(zap.UseDevMode(true))) + + tests := []struct { + name string + advertisement *networkingv1alpha1.ConnectorAdvertisement + connector *networkingv1alpha1.Connector + wantStatus metav1.ConditionStatus + wantReason string + }{ + { + name: "connector reference resolved", + advertisement: &networkingv1alpha1.ConnectorAdvertisement{ + ObjectMeta: metav1.ObjectMeta{Name: "ad", Namespace: "default"}, + Spec: networkingv1alpha1.ConnectorAdvertisementSpec{ + ConnectorRef: networkingv1alpha1.LocalConnectorReference{Name: "connector"}, + }, + }, + connector: &networkingv1alpha1.Connector{ + ObjectMeta: metav1.ObjectMeta{Name: "connector", Namespace: "default"}, + Spec: networkingv1alpha1.ConnectorSpec{ConnectorClassName: "datum-connect"}, + }, + wantStatus: metav1.ConditionTrue, + wantReason: networkingv1alpha1.ConnectorAdvertisementReasonAccepted, + }, + { + name: "connector reference missing", + advertisement: &networkingv1alpha1.ConnectorAdvertisement{ + ObjectMeta: metav1.ObjectMeta{Name: "ad", Namespace: "default"}, + Spec: networkingv1alpha1.ConnectorAdvertisementSpec{ + ConnectorRef: networkingv1alpha1.LocalConnectorReference{Name: "missing"}, + }, + }, + wantStatus: metav1.ConditionFalse, + wantReason: networkingv1alpha1.ConnectorAdvertisementReasonConnectorNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testScheme := runtime.NewScheme() + assert.NoError(t, scheme.AddToScheme(testScheme)) + assert.NoError(t, networkingv1alpha1.AddToScheme(testScheme)) + + builder := fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects(tt.advertisement). + WithStatusSubresource(tt.advertisement) + if tt.connector != nil { + builder = builder.WithObjects(tt.connector) + } + cl := builder.Build() + + reconciler := &ConnectorAdvertisementReconciler{mgr: &fakeMockManager{cl: cl}} + req := mcreconcile.Request{ + Request: reconcile.Request{ + NamespacedName: client.ObjectKeyFromObject(tt.advertisement), + }, + ClusterName: "single", + } + + _, err := reconciler.Reconcile(context.Background(), req) + assert.NoError(t, err) + + var updated networkingv1alpha1.ConnectorAdvertisement + assert.NoError(t, cl.Get(context.Background(), client.ObjectKeyFromObject(tt.advertisement), &updated)) + + condition := apimeta.FindStatusCondition(updated.Status.Conditions, networkingv1alpha1.ConnectorAdvertisementConditionAccepted) + if assert.NotNil(t, condition) { + assert.Equal(t, tt.wantStatus, condition.Status) + assert.Equal(t, tt.wantReason, condition.Reason) + } + if tt.connector != nil { + assert.True(t, metav1.IsControlledBy(&updated, tt.connector)) + } + }) + } +} diff --git a/internal/controller/connectoradvertisement_controller.go b/internal/controller/connectoradvertisement_controller.go new file mode 100644 index 00000000..b0739c6f --- /dev/null +++ b/internal/controller/connectoradvertisement_controller.go @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package controller + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + networkingv1alpha1 "go.datum.net/network-services-operator/api/v1alpha1" +) + +// ConnectorAdvertisementReconciler reconciles a ConnectorAdvertisement object +type ConnectorAdvertisementReconciler struct { + mgr mcmanager.Manager +} + +// +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectoradvertisements,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectoradvertisements/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectoradvertisements/finalizers,verbs=update +// +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectors,verbs=get;list;watch + +func (r *ConnectorAdvertisementReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (_ ctrl.Result, err error) { + logger := log.FromContext(ctx, "cluster", req.ClusterName) + ctx = log.IntoContext(ctx, logger) + + cl, err := r.mgr.GetCluster(ctx, req.ClusterName) + if err != nil { + return ctrl.Result{}, err + } + + var advertisement networkingv1alpha1.ConnectorAdvertisement + if err := cl.GetClient().Get(ctx, req.NamespacedName, &advertisement); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + if !advertisement.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + logger.Info("reconciling connectoradvertisement") + defer logger.Info("reconcile complete") + + originalStatus := advertisement.Status.DeepCopy() + + acceptedCondition := apimeta.FindStatusCondition(advertisement.Status.Conditions, networkingv1alpha1.ConnectorAdvertisementConditionAccepted) + if acceptedCondition == nil { + acceptedCondition = &metav1.Condition{ + Type: networkingv1alpha1.ConnectorAdvertisementConditionAccepted, + } + } + acceptedCondition.ObservedGeneration = advertisement.Generation + + if advertisement.Spec.ConnectorRef.Name == "" { + acceptedCondition.Status = metav1.ConditionFalse + acceptedCondition.Reason = networkingv1alpha1.ConnectorAdvertisementReasonConnectorRefMissing + acceptedCondition.Message = "connectorRef.name is required" + apimeta.SetStatusCondition(&advertisement.Status.Conditions, *acceptedCondition) + } else { + connector := &networkingv1alpha1.Connector{} + connectorKey := client.ObjectKey{Namespace: advertisement.Namespace, Name: advertisement.Spec.ConnectorRef.Name} + if err := cl.GetClient().Get(ctx, connectorKey, connector); err != nil { + if apierrors.IsNotFound(err) { + acceptedCondition.Status = metav1.ConditionFalse + acceptedCondition.Reason = networkingv1alpha1.ConnectorAdvertisementReasonConnectorNotFound + acceptedCondition.Message = fmt.Sprintf("Connector %q not found", advertisement.Spec.ConnectorRef.Name) + apimeta.SetStatusCondition(&advertisement.Status.Conditions, *acceptedCondition) + } else { + return ctrl.Result{}, err + } + } else { + if !metav1.IsControlledBy(&advertisement, connector) { + if err := controllerutil.SetControllerReference(connector, &advertisement, cl.GetScheme()); err != nil { + return ctrl.Result{}, err + } + if err := cl.GetClient().Update(ctx, &advertisement); err != nil { + return ctrl.Result{}, err + } + } + acceptedCondition.Status = metav1.ConditionTrue + acceptedCondition.Reason = networkingv1alpha1.ConnectorAdvertisementReasonAccepted + acceptedCondition.Message = "Connector reference resolved" + apimeta.SetStatusCondition(&advertisement.Status.Conditions, *acceptedCondition) + } + } + + if !equality.Semantic.DeepEqual(*originalStatus, advertisement.Status) { + if statusErr := cl.GetClient().Status().Update(ctx, &advertisement); statusErr != nil { + return ctrl.Result{}, fmt.Errorf("failed updating connectoradvertisement status: %w", statusErr) + } + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ConnectorAdvertisementReconciler) SetupWithManager(mgr mcmanager.Manager) error { + r.mgr = mgr + return mcbuilder.ControllerManagedBy(mgr). + For(&networkingv1alpha1.ConnectorAdvertisement{}). + Named("connectoradvertisement"). + Complete(r) +} From e57501575963b6f2a6be8d6795e719b5aeb4c54f Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Wed, 21 Jan 2026 17:05:08 -0800 Subject: [PATCH 2/7] feat: wire up leases to connector and iam roles --- api/v1alpha1/connector_types.go | 12 ++ api/v1alpha1/connectorclass_types.go | 5 + api/v1alpha1/zz_generated.deepcopy.go | 6 + cmd/main.go | 4 +- ...orking.datumapis.com_connectorclasses.yaml | 9 ++ .../networking.datumapis.com_connectors.yaml | 15 +++ .../protected-resources/kustomization.yaml | 4 + config/iam/protected-resources/leases.yaml | 22 ++++ config/iam/roles/connector-admin.yaml | 24 ++++ config/iam/roles/connector-viewer.yaml | 22 ++++ config/iam/roles/kustomization.yaml | 2 + config/iam/roles/networking-admin.yaml | 1 + config/iam/roles/networking-viewer.yaml | 1 + config/rbac/role.yaml | 12 ++ internal/config/config.go | 11 ++ internal/config/zz_generated.deepcopy.go | 16 +++ internal/config/zz_generated.defaults.go | 3 + internal/controller/connector_controller.go | 110 +++++++++++++++++- .../controller/connector_controllers_test.go | 84 +++++++++++-- 19 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 config/iam/protected-resources/leases.yaml create mode 100644 config/iam/roles/connector-admin.yaml create mode 100644 config/iam/roles/connector-viewer.yaml diff --git a/api/v1alpha1/connector_types.go b/api/v1alpha1/connector_types.go index af2bf4ad..1108c7d0 100644 --- a/api/v1alpha1/connector_types.go +++ b/api/v1alpha1/connector_types.go @@ -3,6 +3,7 @@ package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -136,13 +137,24 @@ type ConnectorStatus struct { // // +kubebuilder:validation:Optional ConnectionDetails *ConnectorConnectionDetails `json:"connectionDetails,omitempty"` + + // LeaseRef references the Lease used to report connector liveness. + // + // +kubebuilder:validation:Optional + LeaseRef *corev1.LocalObjectReference `json:"leaseRef,omitempty"` } const ( // ConnectorConditionAccepted indicates whether the ConnectorClass is resolved. ConnectorConditionAccepted = "Accepted" + // ConnectorConditionReady indicates whether the Connector is ready to tunnel traffic. + ConnectorConditionReady = "Ready" // ConnectorReasonAccepted indicates the Connector is accepted by the controller. ConnectorReasonAccepted = "Accepted" + // ConnectorReasonReady indicates the Connector is ready to tunnel traffic. + ConnectorReasonReady = "ConnectorReady" + // ConnectorReasonNotReady indicates the Connector is not ready to tunnel traffic. + ConnectorReasonNotReady = "ConnectorNotReady" // ConnectorReasonPending indicates the Connector has not been processed yet. ConnectorReasonPending = "Pending" // ConnectorReasonConnectorClassNotFound indicates the referenced class is missing. diff --git a/api/v1alpha1/connectorclass_types.go b/api/v1alpha1/connectorclass_types.go index 4fecea40..82dce56b 100644 --- a/api/v1alpha1/connectorclass_types.go +++ b/api/v1alpha1/connectorclass_types.go @@ -8,6 +8,11 @@ import ( // ConnectorClassSpec defines the desired state of ConnectorClass. type ConnectorClassSpec struct { + // ControllerName is the name of the controller responsible for this ConnectorClass. + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=datum-connect + ControllerName string `json:"controllerName"` } // ConnectorClassStatus defines the observed state of ConnectorClass. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 28dd6d39..5415f1ff 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -7,6 +7,7 @@ package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -462,6 +463,11 @@ func (in *ConnectorStatus) DeepCopyInto(out *ConnectorStatus) { *out = new(ConnectorConnectionDetails) (*in).DeepCopyInto(*out) } + if in.LeaseRef != nil { + in, out := &in.LeaseRef, &out.LeaseRef + *out = new(corev1.LocalObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectorStatus. diff --git a/cmd/main.go b/cmd/main.go index 76fcc891..058ab9f3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -280,7 +280,9 @@ func main() { os.Exit(1) } - if err := (&controller.ConnectorReconciler{}).SetupWithManager(mgr); err != nil { + if err := (&controller.ConnectorReconciler{ + Config: serverConfig, + }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Connector") os.Exit(1) } diff --git a/config/crd/bases/networking.datumapis.com_connectorclasses.yaml b/config/crd/bases/networking.datumapis.com_connectorclasses.yaml index 5c851d36..07978365 100644 --- a/config/crd/bases/networking.datumapis.com_connectorclasses.yaml +++ b/config/crd/bases/networking.datumapis.com_connectorclasses.yaml @@ -38,6 +38,15 @@ spec: type: object spec: description: Spec defines the desired state of a ConnectorClass + properties: + controllerName: + description: ControllerName is the name of the controller responsible + for this ConnectorClass. + enum: + - datum-connect + type: string + required: + - controllerName type: object status: description: Status defines the observed state of a ConnectorClass diff --git a/config/crd/bases/networking.datumapis.com_connectors.yaml b/config/crd/bases/networking.datumapis.com_connectors.yaml index 7d578dc9..2ee75b59 100644 --- a/config/crd/bases/networking.datumapis.com_connectors.yaml +++ b/config/crd/bases/networking.datumapis.com_connectors.yaml @@ -272,6 +272,21 @@ spec: rule: '!(self.type != ''PublicKey'' && has(self.publicKey))' - message: publicKey field must be specified if the type is PublicKey rule: self.type == 'PublicKey' && has(self.publicKey) + leaseRef: + description: LeaseRef references the Lease used to report connector + liveness. + 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 type: object required: - spec diff --git a/config/iam/protected-resources/kustomization.yaml b/config/iam/protected-resources/kustomization.yaml index 6014fa4f..0e826922 100644 --- a/config/iam/protected-resources/kustomization.yaml +++ b/config/iam/protected-resources/kustomization.yaml @@ -22,6 +22,10 @@ resources: - backends.yaml - backendtrafficpolicies.yaml - backendtlspolicies.yaml + - connectoradvertisements.yaml + - connectorclasses.yaml + - connectors.yaml - httproutefilters.yaml + - leases.yaml - securitypolicies.yaml - trafficprotectionpolicies.yaml diff --git a/config/iam/protected-resources/leases.yaml b/config/iam/protected-resources/leases.yaml new file mode 100644 index 00000000..dac703f9 --- /dev/null +++ b/config/iam/protected-resources/leases.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: iam.miloapis.com/v1alpha1 +kind: ProtectedResource +metadata: + name: coordination.k8s.io-lease +spec: + serviceRef: + name: "coordination.k8s.io" + kind: Lease + plural: leases + singular: lease + permissions: + - list + - get + - watch + - create + - update + - patch + - delete + parentResources: + - apiGroup: resourcemanager.miloapis.com + kind: Project diff --git a/config/iam/roles/connector-admin.yaml b/config/iam/roles/connector-admin.yaml new file mode 100644 index 00000000..a1c5e684 --- /dev/null +++ b/config/iam/roles/connector-admin.yaml @@ -0,0 +1,24 @@ +apiVersion: iam.miloapis.com/v1alpha1 +kind: Role +metadata: + name: networking.datumapis.com-connector-admin + annotations: + kubernetes.io/display-name: Connector Admin + kubernetes.io/description: "Full access to connector resources" +spec: + launchStage: Beta + inheritedRoles: + - name: networking.datumapis.com-connector-viewer + includedPermissions: + - networking.datumapis.com/connectors.create + - networking.datumapis.com/connectors.update + - networking.datumapis.com/connectors.patch + - networking.datumapis.com/connectors.delete + - networking.datumapis.com/connectoradvertisements.create + - networking.datumapis.com/connectoradvertisements.update + - networking.datumapis.com/connectoradvertisements.patch + - networking.datumapis.com/connectoradvertisements.delete + - coordination.k8s.io/leases.create + - coordination.k8s.io/leases.update + - coordination.k8s.io/leases.patch + - coordination.k8s.io/leases.delete diff --git a/config/iam/roles/connector-viewer.yaml b/config/iam/roles/connector-viewer.yaml new file mode 100644 index 00000000..e8a9a525 --- /dev/null +++ b/config/iam/roles/connector-viewer.yaml @@ -0,0 +1,22 @@ +apiVersion: iam.miloapis.com/v1alpha1 +kind: Role +metadata: + name: networking.datumapis.com-connector-viewer + annotations: + kubernetes.io/display-name: Connector Viewer + kubernetes.io/description: "View access to connector resources" +spec: + launchStage: Beta + includedPermissions: + - networking.datumapis.com/connectors.list + - networking.datumapis.com/connectors.get + - networking.datumapis.com/connectors.watch + - networking.datumapis.com/connectoradvertisements.list + - networking.datumapis.com/connectoradvertisements.get + - networking.datumapis.com/connectoradvertisements.watch + - networking.datumapis.com/connectorclasses.list + - networking.datumapis.com/connectorclasses.get + - networking.datumapis.com/connectorclasses.watch + - coordination.k8s.io/leases.list + - coordination.k8s.io/leases.get + - coordination.k8s.io/leases.watch diff --git a/config/iam/roles/kustomization.yaml b/config/iam/roles/kustomization.yaml index fea2335f..627fb1f3 100644 --- a/config/iam/roles/kustomization.yaml +++ b/config/iam/roles/kustomization.yaml @@ -4,6 +4,8 @@ # Each role should have a corresponding configuration file in this directory. resources: + - connector-admin.yaml + - connector-viewer.yaml - gateway-admin.yaml - gateway-viewer.yaml - location-admin.yaml diff --git a/config/iam/roles/networking-admin.yaml b/config/iam/roles/networking-admin.yaml index 80016285..24ca519b 100644 --- a/config/iam/roles/networking-admin.yaml +++ b/config/iam/roles/networking-admin.yaml @@ -9,6 +9,7 @@ spec: launchStage: Beta inheritedRoles: - name: networking.datumapis.com-viewer + - name: networking.datumapis.com-connector-admin - name: networking.datumapis.com-gateway-admin - name: networking.datumapis.com-domain-admin includedPermissions: diff --git a/config/iam/roles/networking-viewer.yaml b/config/iam/roles/networking-viewer.yaml index 45804a36..3171c5ec 100644 --- a/config/iam/roles/networking-viewer.yaml +++ b/config/iam/roles/networking-viewer.yaml @@ -8,6 +8,7 @@ metadata: spec: launchStage: Beta inheritedRoles: + - name: networking.datumapis.com-connector-viewer - name: networking.datumapis.com-gateway-viewer - name: networking.datumapis.com-domain-viewer includedPermissions: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 7473da08..9557391c 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -38,6 +38,18 @@ rules: - get - patch - update +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - discovery.k8s.io resources: diff --git a/internal/config/config.go b/internal/config/config.go index 056a9b8b..94588c59 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -44,6 +44,8 @@ type NetworkServicesOperator struct { HTTPProxy HTTPProxyConfig `json:"httpProxy"` + Connector ConnectorConfig `json:"connector"` + Discovery DiscoveryConfig `json:"discovery"` DownstreamResourceManagement DownstreamResourceManagementConfig `json:"downstreamResourceManagement"` @@ -57,6 +59,15 @@ type NetworkServicesOperator struct { DomainRegistration DomainRegistrationConfig `json:"domainRegistration"` } +// +k8s:deepcopy-gen=true +type ConnectorConfig struct { + // LeaseDurationSeconds is the number of seconds the connector lease is valid for. + // + // Defaults to 30 seconds. + // +default=30 + LeaseDurationSeconds int32 `json:"leaseDurationSeconds,omitempty"` +} + // +k8s:deepcopy-gen=true type WebhookServerConfig struct { diff --git a/internal/config/zz_generated.deepcopy.go b/internal/config/zz_generated.deepcopy.go index 2d8417f8..f8da69bb 100644 --- a/internal/config/zz_generated.deepcopy.go +++ b/internal/config/zz_generated.deepcopy.go @@ -95,6 +95,21 @@ func (in *ClusterSettingsValidationOptions) DeepCopy() *ClusterSettingsValidatio return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnectorConfig) DeepCopyInto(out *ConnectorConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectorConfig. +func (in *ConnectorConfig) DeepCopy() *ConnectorConfig { + if in == nil { + return nil + } + out := new(ConnectorConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CorazaConfig) DeepCopyInto(out *CorazaConfig) { *out = *in @@ -397,6 +412,7 @@ func (in *NetworkServicesOperator) DeepCopyInto(out *NetworkServicesOperator) { in.WebhookServer.DeepCopyInto(&out.WebhookServer) in.Gateway.DeepCopyInto(&out.Gateway) out.HTTPProxy = in.HTTPProxy + out.Connector = in.Connector out.Discovery = in.Discovery out.DownstreamResourceManagement = in.DownstreamResourceManagement in.Redis.DeepCopyInto(&out.Redis) diff --git a/internal/config/zz_generated.defaults.go b/internal/config/zz_generated.defaults.go index eec42410..64e6dcd6 100644 --- a/internal/config/zz_generated.defaults.go +++ b/internal/config/zz_generated.defaults.go @@ -221,6 +221,9 @@ func SetObjectDefaults_NetworkServicesOperator(in *NetworkServicesOperator) { if in.HTTPProxy.GatewayClassName == "" { in.HTTPProxy.GatewayClassName = "datum-external-global-proxy" } + if in.Connector.LeaseDurationSeconds == 0 { + in.Connector.LeaseDurationSeconds = 30 + } SetDefaults_DiscoveryConfig(&in.Discovery) if in.Redis.DialTimeout == nil { if err := json.Unmarshal([]byte(`"5s"`), &in.Redis.DialTimeout); err != nil { diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go index aab492b0..fa2291b4 100644 --- a/internal/controller/connector_controller.go +++ b/internal/controller/connector_controller.go @@ -5,30 +5,40 @@ package controller import ( "context" "fmt" + "math/rand" + "time" + coordinationv1 "k8s.io/api/coordination/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" + mchandler "sigs.k8s.io/multicluster-runtime/pkg/handler" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" networkingv1alpha1 "go.datum.net/network-services-operator/api/v1alpha1" + "go.datum.net/network-services-operator/internal/config" ) // ConnectorReconciler reconciles a Connector object type ConnectorReconciler struct { - mgr mcmanager.Manager + mgr mcmanager.Manager + Config config.NetworkServicesOperator } // +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectors,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectors/status,verbs=get;update;patch // +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectors/finalizers,verbs=update // +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectorclasses,verbs=get;list;watch +// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch;delete func (r *ConnectorReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (_ ctrl.Result, err error) { logger := log.FromContext(ctx, "cluster", req.ClusterName) @@ -55,6 +65,13 @@ func (r *ConnectorReconciler) Reconcile(ctx context.Context, req mcreconcile.Req defer logger.Info("reconcile complete") originalStatus := connector.Status.DeepCopy() + readyCondition := apimeta.FindStatusCondition(connector.Status.Conditions, networkingv1alpha1.ConnectorConditionReady) + if readyCondition == nil { + readyCondition = &metav1.Condition{ + Type: networkingv1alpha1.ConnectorConditionReady, + } + } + readyCondition.ObservedGeneration = connector.Generation acceptedCondition := apimeta.FindStatusCondition(connector.Status.Conditions, networkingv1alpha1.ConnectorConditionAccepted) if acceptedCondition == nil { @@ -86,6 +103,44 @@ func (r *ConnectorReconciler) Reconcile(ctx context.Context, req mcreconcile.Req } apimeta.SetStatusCondition(&connector.Status.Conditions, *acceptedCondition) + apimeta.SetStatusCondition(&connector.Status.Conditions, *readyCondition) + + leaseDurationSeconds := r.connectorLeaseDurationSeconds() + if connector.Status.LeaseRef == nil || connector.Status.LeaseRef.Name == "" { + lease := &coordinationv1.Lease{ + ObjectMeta: metav1.ObjectMeta{ + Name: connector.Name, + Namespace: connector.Namespace, + }, + } + + if _, err := controllerutil.CreateOrUpdate(ctx, cl.GetClient(), lease, func() error { + if err := controllerutil.SetControllerReference(&connector, lease, cl.GetScheme()); err != nil { + return err + } + if lease.Spec.LeaseDurationSeconds == nil || *lease.Spec.LeaseDurationSeconds == 0 { + lease.Spec.LeaseDurationSeconds = &leaseDurationSeconds + } + return nil + }); err != nil { + return ctrl.Result{}, err + } + + connector.Status.LeaseRef = &corev1.LocalObjectReference{Name: lease.Name} + } + + leaseReady, leaseMessage, requeueAfter := r.connectorLeaseReady(ctx, cl.GetClient(), &connector) + if leaseReady { + readyCondition.Status = metav1.ConditionTrue + readyCondition.Reason = networkingv1alpha1.ConnectorReasonReady + readyCondition.Message = "The connector is ready to tunnel traffic." + } else { + readyCondition.Status = metav1.ConditionFalse + readyCondition.Reason = networkingv1alpha1.ConnectorReasonNotReady + readyCondition.Message = leaseMessage + } + + apimeta.SetStatusCondition(&connector.Status.Conditions, *readyCondition) if !equality.Semantic.DeepEqual(*originalStatus, connector.Status) { if statusErr := cl.GetClient().Status().Update(ctx, &connector); statusErr != nil { @@ -93,14 +148,67 @@ func (r *ConnectorReconciler) Reconcile(ctx context.Context, req mcreconcile.Req } } + if requeueAfter != nil { + return ctrl.Result{RequeueAfter: *requeueAfter}, nil + } return ctrl.Result{}, nil } +func (r *ConnectorReconciler) connectorLeaseDurationSeconds() int32 { + if r.Config.Connector.LeaseDurationSeconds > 0 { + return r.Config.Connector.LeaseDurationSeconds + } + return 30 +} + +func (r *ConnectorReconciler) connectorLeaseReady(ctx context.Context, cl client.Client, connector *networkingv1alpha1.Connector) (bool, string, *time.Duration) { + if connector.Status.LeaseRef == nil || connector.Status.LeaseRef.Name == "" { + return false, "Connector lease has not been created yet.", nil + } + + var lease coordinationv1.Lease + if err := cl.Get(ctx, client.ObjectKey{Namespace: connector.Namespace, Name: connector.Status.LeaseRef.Name}, &lease); err != nil { + if apierrors.IsNotFound(err) { + return false, "Connector lease not found. Agent may be offline.", nil + } + return false, fmt.Sprintf("Failed to load connector lease: %v", err), nil + } + + if lease.Spec.RenewTime == nil || lease.Spec.LeaseDurationSeconds == nil { + return false, "Connector lease has not been renewed yet.", nil + } + + expiryDuration := time.Duration(*lease.Spec.LeaseDurationSeconds) * time.Second + expiresAt := lease.Spec.RenewTime.Time.Add(expiryDuration) + if time.Now().After(expiresAt) { + return false, "Connector lease has expired. Agent may be offline.", nil + } + + requeueAfter := expiresAt.Sub(time.Now()) + leaseJitter(expiryDuration) + return true, "", &requeueAfter +} + +func leaseJitter(base time.Duration) time.Duration { + if base <= 0 { + return 0 + } + jitterMax := base / 10 + if jitterMax <= 0 { + return 0 + } + return time.Duration(rand.Int63n(int64(jitterMax))) +} + // SetupWithManager sets up the controller with the Manager. func (r *ConnectorReconciler) SetupWithManager(mgr mcmanager.Manager) error { r.mgr = mgr + return mcbuilder.ControllerManagedBy(mgr). For(&networkingv1alpha1.Connector{}). + Watches( + &coordinationv1.Lease{}, + mchandler.EnqueueRequestForOwner(&networkingv1alpha1.Connector{}, handler.OnlyControllerOwner()), + ). Named("connector"). Complete(r) } diff --git a/internal/controller/connector_controllers_test.go b/internal/controller/connector_controllers_test.go index 8da7d516..7e2f2ebb 100644 --- a/internal/controller/connector_controllers_test.go +++ b/internal/controller/connector_controllers_test.go @@ -3,12 +3,15 @@ package controller import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" + coordinationv1 "k8s.io/api/coordination/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/log" @@ -23,11 +26,14 @@ func TestConnectorReconcile(t *testing.T) { log.SetLogger(zap.New(zap.UseDevMode(true))) tests := []struct { - name string - connector *networkingv1alpha1.Connector - connectorClass *networkingv1alpha1.ConnectorClass - wantStatus metav1.ConditionStatus - wantReason string + name string + connector *networkingv1alpha1.Connector + connectorClass *networkingv1alpha1.ConnectorClass + lease *coordinationv1.Lease + wantStatus metav1.ConditionStatus + wantReason string + wantReady metav1.ConditionStatus + wantReadyReason string }{ { name: "connector class resolved", @@ -40,8 +46,10 @@ func TestConnectorReconcile(t *testing.T) { connectorClass: &networkingv1alpha1.ConnectorClass{ ObjectMeta: metav1.ObjectMeta{Name: "datum-connect"}, }, - wantStatus: metav1.ConditionTrue, - wantReason: networkingv1alpha1.ConnectorReasonAccepted, + wantStatus: metav1.ConditionTrue, + wantReason: networkingv1alpha1.ConnectorReasonAccepted, + wantReady: metav1.ConditionFalse, + wantReadyReason: networkingv1alpha1.ConnectorReasonNotReady, }, { name: "connector class missing", @@ -51,8 +59,56 @@ func TestConnectorReconcile(t *testing.T) { ConnectorClassName: "missing", }, }, - wantStatus: metav1.ConditionFalse, - wantReason: networkingv1alpha1.ConnectorReasonConnectorClassNotFound, + wantStatus: metav1.ConditionFalse, + wantReason: networkingv1alpha1.ConnectorReasonConnectorClassNotFound, + wantReady: metav1.ConditionFalse, + wantReadyReason: networkingv1alpha1.ConnectorReasonNotReady, + }, + { + name: "connector lease ready", + connector: &networkingv1alpha1.Connector{ + ObjectMeta: metav1.ObjectMeta{Name: "connector", Namespace: "default"}, + Spec: networkingv1alpha1.ConnectorSpec{ + ConnectorClassName: "datum-connect", + }, + }, + connectorClass: &networkingv1alpha1.ConnectorClass{ + ObjectMeta: metav1.ObjectMeta{Name: "datum-connect"}, + }, + lease: &coordinationv1.Lease{ + ObjectMeta: metav1.ObjectMeta{Name: "connector", Namespace: "default"}, + Spec: coordinationv1.LeaseSpec{ + LeaseDurationSeconds: ptr.To[int32](30), + RenewTime: &metav1.MicroTime{Time: time.Now()}, + }, + }, + wantStatus: metav1.ConditionTrue, + wantReason: networkingv1alpha1.ConnectorReasonAccepted, + wantReady: metav1.ConditionTrue, + wantReadyReason: networkingv1alpha1.ConnectorReasonReady, + }, + { + name: "connector lease expired", + connector: &networkingv1alpha1.Connector{ + ObjectMeta: metav1.ObjectMeta{Name: "connector", Namespace: "default"}, + Spec: networkingv1alpha1.ConnectorSpec{ + ConnectorClassName: "datum-connect", + }, + }, + connectorClass: &networkingv1alpha1.ConnectorClass{ + ObjectMeta: metav1.ObjectMeta{Name: "datum-connect"}, + }, + lease: &coordinationv1.Lease{ + ObjectMeta: metav1.ObjectMeta{Name: "connector", Namespace: "default"}, + Spec: coordinationv1.LeaseSpec{ + LeaseDurationSeconds: ptr.To[int32](30), + RenewTime: &metav1.MicroTime{Time: time.Now().Add(-time.Minute)}, + }, + }, + wantStatus: metav1.ConditionTrue, + wantReason: networkingv1alpha1.ConnectorReasonAccepted, + wantReady: metav1.ConditionFalse, + wantReadyReason: networkingv1alpha1.ConnectorReasonNotReady, }, } @@ -60,12 +116,16 @@ func TestConnectorReconcile(t *testing.T) { t.Run(tt.name, func(t *testing.T) { testScheme := runtime.NewScheme() assert.NoError(t, scheme.AddToScheme(testScheme)) + assert.NoError(t, coordinationv1.AddToScheme(testScheme)) assert.NoError(t, networkingv1alpha1.AddToScheme(testScheme)) builder := fake.NewClientBuilder().WithScheme(testScheme).WithObjects(tt.connector) if tt.connectorClass != nil { builder = builder.WithObjects(tt.connectorClass) } + if tt.lease != nil { + builder = builder.WithObjects(tt.lease) + } builder = builder.WithStatusSubresource(tt.connector) cl := builder.Build() @@ -88,6 +148,12 @@ func TestConnectorReconcile(t *testing.T) { assert.Equal(t, tt.wantStatus, condition.Status) assert.Equal(t, tt.wantReason, condition.Reason) } + + readyCondition := apimeta.FindStatusCondition(updated.Status.Conditions, networkingv1alpha1.ConnectorConditionReady) + if assert.NotNil(t, readyCondition) { + assert.Equal(t, tt.wantReady, readyCondition.Status) + assert.Equal(t, tt.wantReadyReason, readyCondition.Reason) + } }) } } From 369e30b65b6158f9b9297766ea6e4e624e50b8a3 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Wed, 21 Jan 2026 17:13:54 -0800 Subject: [PATCH 3/7] feat: wire up leases to connector and iam roles --- api/v1alpha1/connector_types.go | 6 +++--- api/v1alpha1/connectoradvertisement_types.go | 2 -- api/v1alpha1/object_reference_types.go | 1 + .../networking.datumapis.com_connectoradvertisements.yaml | 1 + config/crd/bases/networking.datumapis.com_connectors.yaml | 3 +++ internal/controller/connector_controller.go | 2 +- internal/controller/connectoradvertisement_controller.go | 2 +- 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/connector_types.go b/api/v1alpha1/connector_types.go index 1108c7d0..27a23ac7 100644 --- a/api/v1alpha1/connector_types.go +++ b/api/v1alpha1/connector_types.go @@ -34,7 +34,9 @@ type ConnectorCapability struct { // ConnectorSpec defines the desired state of Connector. type ConnectorSpec struct { - ConnectorClassName string `json:"connectorClassName,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + ConnectorClassName string `json:"connectorClassName"` // Capabilities desired to be supported by the connector. // @@ -159,8 +161,6 @@ const ( ConnectorReasonPending = "Pending" // ConnectorReasonConnectorClassNotFound indicates the referenced class is missing. ConnectorReasonConnectorClassNotFound = "ConnectorClassNotFound" - // ConnectorReasonConnectorClassNotSpecified indicates the class name is empty. - ConnectorReasonConnectorClassNotSpecified = "ConnectorClassNotSpecified" ) const ConnectorNameAnnotation = "networking.datum.org/connector-name" diff --git a/api/v1alpha1/connectoradvertisement_types.go b/api/v1alpha1/connectoradvertisement_types.go index 74d1c505..2f3605ed 100644 --- a/api/v1alpha1/connectoradvertisement_types.go +++ b/api/v1alpha1/connectoradvertisement_types.go @@ -113,8 +113,6 @@ const ( ConnectorAdvertisementReasonPending = "Pending" // ConnectorAdvertisementReasonConnectorNotFound indicates the referenced connector is missing. ConnectorAdvertisementReasonConnectorNotFound = "ConnectorNotFound" - // ConnectorAdvertisementReasonConnectorRefMissing indicates the connectorRef is missing or empty. - ConnectorAdvertisementReasonConnectorRefMissing = "ConnectorRefMissing" ) // +kubebuilder:object:root=true diff --git a/api/v1alpha1/object_reference_types.go b/api/v1alpha1/object_reference_types.go index 71cb8dfa..fed4a5b9 100644 --- a/api/v1alpha1/object_reference_types.go +++ b/api/v1alpha1/object_reference_types.go @@ -4,5 +4,6 @@ type LocalConnectorReference struct { // Name of the referenced Connector. // // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 Name string `json:"name"` } diff --git a/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml b/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml index b3b5fb2a..d78bd6d3 100644 --- a/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml +++ b/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml @@ -45,6 +45,7 @@ spec: properties: name: description: Name of the referenced Connector. + minLength: 1 type: string required: - name diff --git a/config/crd/bases/networking.datumapis.com_connectors.yaml b/config/crd/bases/networking.datumapis.com_connectors.yaml index 2ee75b59..f49936dc 100644 --- a/config/crd/bases/networking.datumapis.com_connectors.yaml +++ b/config/crd/bases/networking.datumapis.com_connectors.yaml @@ -68,7 +68,10 @@ spec: - type x-kubernetes-list-type: map connectorClassName: + minLength: 1 type: string + required: + - connectorClassName type: object status: default: diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go index fa2291b4..d1d060fa 100644 --- a/internal/controller/connector_controller.go +++ b/internal/controller/connector_controller.go @@ -83,7 +83,7 @@ func (r *ConnectorReconciler) Reconcile(ctx context.Context, req mcreconcile.Req if connector.Spec.ConnectorClassName == "" { acceptedCondition.Status = metav1.ConditionFalse - acceptedCondition.Reason = networkingv1alpha1.ConnectorReasonConnectorClassNotSpecified + acceptedCondition.Reason = networkingv1alpha1.ConnectorReasonPending acceptedCondition.Message = "connectorClassName is required" } else { var connectorClass networkingv1alpha1.ConnectorClass diff --git a/internal/controller/connectoradvertisement_controller.go b/internal/controller/connectoradvertisement_controller.go index b0739c6f..9821d6d8 100644 --- a/internal/controller/connectoradvertisement_controller.go +++ b/internal/controller/connectoradvertisement_controller.go @@ -67,7 +67,7 @@ func (r *ConnectorAdvertisementReconciler) Reconcile(ctx context.Context, req mc if advertisement.Spec.ConnectorRef.Name == "" { acceptedCondition.Status = metav1.ConditionFalse - acceptedCondition.Reason = networkingv1alpha1.ConnectorAdvertisementReasonConnectorRefMissing + acceptedCondition.Reason = networkingv1alpha1.ConnectorAdvertisementReasonPending acceptedCondition.Message = "connectorRef.name is required" apimeta.SetStatusCondition(&advertisement.Status.Conditions, *acceptedCondition) } else { From b0a075a08c261b52934a6a02dbada08e1827cf20 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Wed, 21 Jan 2026 17:17:40 -0800 Subject: [PATCH 4/7] fix: lint --- internal/controller/connector_controller.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go index d1d060fa..65e4000a 100644 --- a/internal/controller/connector_controller.go +++ b/internal/controller/connector_controller.go @@ -179,12 +179,12 @@ func (r *ConnectorReconciler) connectorLeaseReady(ctx context.Context, cl client } expiryDuration := time.Duration(*lease.Spec.LeaseDurationSeconds) * time.Second - expiresAt := lease.Spec.RenewTime.Time.Add(expiryDuration) + expiresAt := lease.Spec.RenewTime.Add(expiryDuration) if time.Now().After(expiresAt) { return false, "Connector lease has expired. Agent may be offline.", nil } - requeueAfter := expiresAt.Sub(time.Now()) + leaseJitter(expiryDuration) + requeueAfter := time.Until(expiresAt) + leaseJitter(expiryDuration) return true, "", &requeueAfter } From b4fac33390c9214483ec10b9d0a11fedb44ab047 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 22 Jan 2026 12:45:27 -0800 Subject: [PATCH 5/7] refactor: clean up and ads watcher for connector --- api/v1alpha1/connectoradvertisement_types.go | 6 +++ api/v1alpha1/connectorclass_types.go | 4 +- api/v1alpha1/object_reference_types.go | 1 + ...datumapis.com_connectoradvertisements.yaml | 11 +++- ...orking.datumapis.com_connectorclasses.yaml | 5 +- internal/controller/connector_controller.go | 33 +++++++----- .../connectoradvertisement_controller.go | 53 +++++++++++++++++++ 7 files changed, 97 insertions(+), 16 deletions(-) diff --git a/api/v1alpha1/connectoradvertisement_types.go b/api/v1alpha1/connectoradvertisement_types.go index 2f3605ed..95aca442 100644 --- a/api/v1alpha1/connectoradvertisement_types.go +++ b/api/v1alpha1/connectoradvertisement_types.go @@ -99,6 +99,11 @@ type ConnectorAdvertisementSpec struct { type ConnectorAdvertisementStatus struct { // Conditions describe the current conditions of the ConnectorAdvertisement. // + // Known conditions: + // - Accepted: indicates whether the referenced Connector has been resolved. + // When Accepted is False, the reason will explain why the reference + // could not be resolved (for example, ConnectorNotFound). + // // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:"conditions,omitempty"` @@ -117,6 +122,7 @@ const ( // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:selectablefield:JSONPath=".spec.connectorRef.name" // ConnectorAdvertisement is the Schema for the connectoradvertisements API. type ConnectorAdvertisement struct { diff --git a/api/v1alpha1/connectorclass_types.go b/api/v1alpha1/connectorclass_types.go index 82dce56b..29b33cec 100644 --- a/api/v1alpha1/connectorclass_types.go +++ b/api/v1alpha1/connectorclass_types.go @@ -11,7 +11,9 @@ type ConnectorClassSpec struct { // ControllerName is the name of the controller responsible for this ConnectorClass. // // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=datum-connect + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:default=networking.datumapis.com/datum-connect ControllerName string `json:"controllerName"` } diff --git a/api/v1alpha1/object_reference_types.go b/api/v1alpha1/object_reference_types.go index fed4a5b9..ba22afa4 100644 --- a/api/v1alpha1/object_reference_types.go +++ b/api/v1alpha1/object_reference_types.go @@ -5,5 +5,6 @@ type LocalConnectorReference struct { // // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 Name string `json:"name"` } diff --git a/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml b/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml index d78bd6d3..37880343 100644 --- a/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml +++ b/config/crd/bases/networking.datumapis.com_connectoradvertisements.yaml @@ -45,6 +45,7 @@ spec: properties: name: description: Name of the referenced Connector. + maxLength: 253 minLength: 1 type: string required: @@ -134,7 +135,13 @@ spec: description: Status defines the observed state of a ConnectorAdvertisement properties: conditions: - description: Conditions describe the current conditions of the ConnectorAdvertisement. + description: |- + Conditions describe the current conditions of the ConnectorAdvertisement. + + Known conditions: + - Accepted: indicates whether the referenced Connector has been resolved. + When Accepted is False, the reason will explain why the reference + could not be resolved (for example, ConnectorNotFound). items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -197,6 +204,8 @@ spec: required: - spec type: object + selectableFields: + - jsonPath: .spec.connectorRef.name served: true storage: true subresources: diff --git a/config/crd/bases/networking.datumapis.com_connectorclasses.yaml b/config/crd/bases/networking.datumapis.com_connectorclasses.yaml index 07978365..c507a5bb 100644 --- a/config/crd/bases/networking.datumapis.com_connectorclasses.yaml +++ b/config/crd/bases/networking.datumapis.com_connectorclasses.yaml @@ -40,10 +40,11 @@ spec: description: Spec defines the desired state of a ConnectorClass properties: controllerName: + default: networking.datumapis.com/datum-connect description: ControllerName is the name of the controller responsible for this ConnectorClass. - enum: - - datum-connect + maxLength: 253 + minLength: 1 type: string required: - controllerName diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go index 65e4000a..73033990 100644 --- a/internal/controller/connector_controller.go +++ b/internal/controller/connector_controller.go @@ -129,15 +129,18 @@ func (r *ConnectorReconciler) Reconcile(ctx context.Context, req mcreconcile.Req connector.Status.LeaseRef = &corev1.LocalObjectReference{Name: lease.Name} } - leaseReady, leaseMessage, requeueAfter := r.connectorLeaseReady(ctx, cl.GetClient(), &connector) - if leaseReady { + leaseStatus, err := r.connectorLeaseReady(ctx, cl.GetClient(), &connector) + if err != nil { + return ctrl.Result{}, err + } + if leaseStatus.ready { readyCondition.Status = metav1.ConditionTrue readyCondition.Reason = networkingv1alpha1.ConnectorReasonReady readyCondition.Message = "The connector is ready to tunnel traffic." } else { readyCondition.Status = metav1.ConditionFalse readyCondition.Reason = networkingv1alpha1.ConnectorReasonNotReady - readyCondition.Message = leaseMessage + readyCondition.Message = leaseStatus.message } apimeta.SetStatusCondition(&connector.Status.Conditions, *readyCondition) @@ -148,8 +151,8 @@ func (r *ConnectorReconciler) Reconcile(ctx context.Context, req mcreconcile.Req } } - if requeueAfter != nil { - return ctrl.Result{RequeueAfter: *requeueAfter}, nil + if leaseStatus.requeueAfter != nil { + return ctrl.Result{RequeueAfter: *leaseStatus.requeueAfter}, nil } return ctrl.Result{}, nil } @@ -161,31 +164,37 @@ func (r *ConnectorReconciler) connectorLeaseDurationSeconds() int32 { return 30 } -func (r *ConnectorReconciler) connectorLeaseReady(ctx context.Context, cl client.Client, connector *networkingv1alpha1.Connector) (bool, string, *time.Duration) { +type connectorLeaseStatus struct { + ready bool + requeueAfter *time.Duration + message string +} + +func (r *ConnectorReconciler) connectorLeaseReady(ctx context.Context, cl client.Client, connector *networkingv1alpha1.Connector) (connectorLeaseStatus, error) { if connector.Status.LeaseRef == nil || connector.Status.LeaseRef.Name == "" { - return false, "Connector lease has not been created yet.", nil + return connectorLeaseStatus{message: "Connector lease has not been created yet."}, nil } var lease coordinationv1.Lease if err := cl.Get(ctx, client.ObjectKey{Namespace: connector.Namespace, Name: connector.Status.LeaseRef.Name}, &lease); err != nil { if apierrors.IsNotFound(err) { - return false, "Connector lease not found. Agent may be offline.", nil + return connectorLeaseStatus{message: "Connector lease not found. Agent may be offline."}, nil } - return false, fmt.Sprintf("Failed to load connector lease: %v", err), nil + return connectorLeaseStatus{}, fmt.Errorf("failed to load connector lease: %w", err) } if lease.Spec.RenewTime == nil || lease.Spec.LeaseDurationSeconds == nil { - return false, "Connector lease has not been renewed yet.", nil + return connectorLeaseStatus{message: "Connector lease has not been renewed yet."}, nil } expiryDuration := time.Duration(*lease.Spec.LeaseDurationSeconds) * time.Second expiresAt := lease.Spec.RenewTime.Add(expiryDuration) if time.Now().After(expiresAt) { - return false, "Connector lease has expired. Agent may be offline.", nil + return connectorLeaseStatus{message: "Connector lease has expired. Agent may be offline."}, nil } requeueAfter := time.Until(expiresAt) + leaseJitter(expiryDuration) - return true, "", &requeueAfter + return connectorLeaseStatus{ready: true, requeueAfter: &requeueAfter}, nil } func leaseJitter(base time.Duration) time.Duration { diff --git a/internal/controller/connectoradvertisement_controller.go b/internal/controller/connectoradvertisement_controller.go index 9821d6d8..cee9fd60 100644 --- a/internal/controller/connectoradvertisement_controller.go +++ b/internal/controller/connectoradvertisement_controller.go @@ -12,7 +12,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" @@ -26,6 +28,8 @@ type ConnectorAdvertisementReconciler struct { mgr mcmanager.Manager } +const connectorAdvertisementConnectorRefIndex = "spec.connectorRef.name" + // +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectoradvertisements,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectoradvertisements/status,verbs=get;update;patch // +kubebuilder:rbac:groups=networking.datumapis.com,resources=connectoradvertisements/finalizers,verbs=update @@ -110,8 +114,57 @@ func (r *ConnectorAdvertisementReconciler) Reconcile(ctx context.Context, req mc // SetupWithManager sets up the controller with the Manager. func (r *ConnectorAdvertisementReconciler) SetupWithManager(mgr mcmanager.Manager) error { r.mgr = mgr + if err := mgr.GetFieldIndexer().IndexField( + context.Background(), + &networkingv1alpha1.ConnectorAdvertisement{}, + connectorAdvertisementConnectorRefIndex, + func(obj client.Object) []string { + advertisement, ok := obj.(*networkingv1alpha1.ConnectorAdvertisement) + if !ok || advertisement.Spec.ConnectorRef.Name == "" { + return nil + } + return []string{advertisement.Spec.ConnectorRef.Name} + }, + ); err != nil { + return err + } return mcbuilder.ControllerManagedBy(mgr). For(&networkingv1alpha1.ConnectorAdvertisement{}). + Watches( + &networkingv1alpha1.Connector{}, + func(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[client.Object, mcreconcile.Request] { + return handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []mcreconcile.Request { + logger := log.FromContext(ctx) + connector, ok := obj.(*networkingv1alpha1.Connector) + if !ok { + return nil + } + + var ads networkingv1alpha1.ConnectorAdvertisementList + if err := cl.GetClient().List( + ctx, + &ads, + client.InNamespace(connector.Namespace), + client.MatchingFields{connectorAdvertisementConnectorRefIndex: connector.Name}, + ); err != nil { + logger.Error(err, "failed to list connectoradvertisements", "connector", connector.Name) + return nil + } + + requests := make([]mcreconcile.Request, 0, len(ads.Items)) + for i := range ads.Items { + requests = append(requests, mcreconcile.Request{ + ClusterName: clusterName, + Request: ctrl.Request{ + NamespacedName: client.ObjectKeyFromObject(&ads.Items[i]), + }, + }) + } + + return requests + }) + }, + ). Named("connectoradvertisement"). Complete(r) } From 66c3c6b8fd8edc1014c34b1441ec6f82f15a88e2 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 22 Jan 2026 12:54:36 -0800 Subject: [PATCH 6/7] fix: scope down lease rbac --- config/iam/protected-resources/leases.yaml | 4 ---- config/iam/roles/connector-admin.yaml | 2 -- config/iam/roles/connector-viewer.yaml | 2 -- 3 files changed, 8 deletions(-) diff --git a/config/iam/protected-resources/leases.yaml b/config/iam/protected-resources/leases.yaml index dac703f9..30876e3b 100644 --- a/config/iam/protected-resources/leases.yaml +++ b/config/iam/protected-resources/leases.yaml @@ -10,13 +10,9 @@ spec: plural: leases singular: lease permissions: - - list - get - - watch - - create - update - patch - - delete parentResources: - apiGroup: resourcemanager.miloapis.com kind: Project diff --git a/config/iam/roles/connector-admin.yaml b/config/iam/roles/connector-admin.yaml index a1c5e684..3fab56c1 100644 --- a/config/iam/roles/connector-admin.yaml +++ b/config/iam/roles/connector-admin.yaml @@ -18,7 +18,5 @@ spec: - networking.datumapis.com/connectoradvertisements.update - networking.datumapis.com/connectoradvertisements.patch - networking.datumapis.com/connectoradvertisements.delete - - coordination.k8s.io/leases.create - coordination.k8s.io/leases.update - coordination.k8s.io/leases.patch - - coordination.k8s.io/leases.delete diff --git a/config/iam/roles/connector-viewer.yaml b/config/iam/roles/connector-viewer.yaml index e8a9a525..6f1b7de8 100644 --- a/config/iam/roles/connector-viewer.yaml +++ b/config/iam/roles/connector-viewer.yaml @@ -17,6 +17,4 @@ spec: - networking.datumapis.com/connectorclasses.list - networking.datumapis.com/connectorclasses.get - networking.datumapis.com/connectorclasses.watch - - coordination.k8s.io/leases.list - coordination.k8s.io/leases.get - - coordination.k8s.io/leases.watch From 6d2ffda8f8b8aa6143c67ddc82410b999b53c6d6 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 22 Jan 2026 13:17:26 -0800 Subject: [PATCH 7/7] chore: update doc and move lease get into admin --- api/v1alpha1/connector_types.go | 4 ++++ config/crd/bases/networking.datumapis.com_connectors.yaml | 8 ++++++-- config/iam/roles/connector-admin.yaml | 1 + config/iam/roles/connector-viewer.yaml | 1 - 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/connector_types.go b/api/v1alpha1/connector_types.go index 27a23ac7..d7b0c54c 100644 --- a/api/v1alpha1/connector_types.go +++ b/api/v1alpha1/connector_types.go @@ -142,6 +142,10 @@ type ConnectorStatus struct { // LeaseRef references the Lease used to report connector liveness. // + // The connector controller creates the Lease when a Connector is created + // and records it here. Connector implementations (agents) are expected to + // periodically renew the Lease to indicate liveness. + // // +kubebuilder:validation:Optional LeaseRef *corev1.LocalObjectReference `json:"leaseRef,omitempty"` } diff --git a/config/crd/bases/networking.datumapis.com_connectors.yaml b/config/crd/bases/networking.datumapis.com_connectors.yaml index f49936dc..bcc9c7b7 100644 --- a/config/crd/bases/networking.datumapis.com_connectors.yaml +++ b/config/crd/bases/networking.datumapis.com_connectors.yaml @@ -276,8 +276,12 @@ spec: - message: publicKey field must be specified if the type is PublicKey rule: self.type == 'PublicKey' && has(self.publicKey) leaseRef: - description: LeaseRef references the Lease used to report connector - liveness. + description: |- + LeaseRef references the Lease used to report connector liveness. + + The connector controller creates the Lease when a Connector is created + and records it here. Connector implementations (agents) are expected to + periodically renew the Lease to indicate liveness. properties: name: default: "" diff --git a/config/iam/roles/connector-admin.yaml b/config/iam/roles/connector-admin.yaml index 3fab56c1..0f1c5eb1 100644 --- a/config/iam/roles/connector-admin.yaml +++ b/config/iam/roles/connector-admin.yaml @@ -20,3 +20,4 @@ spec: - networking.datumapis.com/connectoradvertisements.delete - coordination.k8s.io/leases.update - coordination.k8s.io/leases.patch + - coordination.k8s.io/leases.get diff --git a/config/iam/roles/connector-viewer.yaml b/config/iam/roles/connector-viewer.yaml index 6f1b7de8..61080466 100644 --- a/config/iam/roles/connector-viewer.yaml +++ b/config/iam/roles/connector-viewer.yaml @@ -17,4 +17,3 @@ spec: - networking.datumapis.com/connectorclasses.list - networking.datumapis.com/connectorclasses.get - networking.datumapis.com/connectorclasses.watch - - coordination.k8s.io/leases.get