Skip to content
Open
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
5 changes: 5 additions & 0 deletions api/core/v1alpha1/device_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import (

// DeviceSpec defines the desired state of Device.
type DeviceSpec struct {
// Paused can be used to prevent controllers from processing the Device and its associated objects.
// +optional
Paused *bool `json:"paused,omitempty"`

// Endpoint contains the connection information for the device.
// +required
Endpoint Endpoint `json:"endpoint"`
Expand Down Expand Up @@ -272,6 +276,7 @@ const (
// +kubebuilder:printcolumn:name="SerialNumber",type=string,JSONPath=".status.serialNumber",priority=1
// +kubebuilder:printcolumn:name="FirmwareVersion",type=string,JSONPath=".status.firmwareVersion",priority=1
// +kubebuilder:printcolumn:name="Ports",type=string,JSONPath=".status.portSummary",priority=1
// +kubebuilder:printcolumn:name="Paused",type=boolean,JSONPath=`.spec.paused`,priority=1
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
Expand Down
5 changes: 5 additions & 0 deletions api/core/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ var (
// with reconciliation of the object only if this label and a configured value is present.
const WatchLabel = "networking.metal.ironcore.dev/watch-filter"

// PausedAnnotation is an annotation that can be applied to any Network API object
// to prevent a controller from processing it. Controllers working with Network API objects
// must check the existence of this annotation on the reconciled object.
const PausedAnnotation = "networking.metal.ironcore.dev/paused"

// FinalizerName is the identifier used by the controllers to perform cleanup before a resource is deleted.
// It is added when the resource is created and ensures that the controller can handle teardown logic
// (e.g., deleting external dependencies) before Kubernetes finalizes the deletion.
Expand Down
5 changes: 5 additions & 0 deletions api/core/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 @@ -46,6 +46,10 @@ spec:
name: Ports
priority: 1
type: string
- jsonPath: .spec.paused
name: Paused
priority: 1
type: boolean
- jsonPath: .status.phase
name: Phase
type: string
Expand Down Expand Up @@ -184,6 +188,10 @@ spec:
x-kubernetes-validations:
- message: SecretRef is required once set
rule: '!has(oldSelf.secretRef) || has(self.secretRef)'
paused:
description: Paused can be used to prevent controllers from processing
the Device and its associated objects.
type: boolean
provisioning:
description: |-
Provisioning is an optional configuration for the device provisioning process.
Expand Down
15 changes: 12 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func main() {
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)
var watchNamespace string
var watchFilterValue string
var providerName string
var requeueInterval time.Duration
Expand All @@ -97,12 +98,13 @@ func main() {
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers")
flag.StringVar(&watchNamespace, "namespace", "", "Namespace that the controller watches to reconcile api objects. If unspecified, the controller watches for api objects across all namespaces.")
flag.StringVar(&watchFilterValue, "watch-filter", "", fmt.Sprintf("Label value that the controller watches to reconcile api objects. Label key is always %q. If unspecified, the controller watches for all api objects.", v1alpha1.WatchLabel))
flag.StringVar(&providerName, "provider", "openconfig", "The provider to use for the controller. If not specified, the default provider is used. Available providers: "+strings.Join(provider.Providers(), ", "))
flag.DurationVar(&requeueInterval, "requeue-interval", 30*time.Second, "The interval after which Kubernetes resources should be reconciled again regardless of whether they have changed.")
flag.DurationVar(&requeueInterval, "requeue-interval", time.Hour, "The interval after which Kubernetes resources should be reconciled again regardless of whether they have changed.")
flag.IntVar(&maxConcurrentReconciles, "max-concurrent-reconciles", 1, "The maximum number of concurrent reconciles per controller. Defaults to 1.")
flag.StringVar(&lockerNamespace, "locker-namespace", "", "The namespace to use for resource locker coordination. If not specified, uses the namespace the manager is deployed in, or 'default' if undetectable.")
flag.DurationVar(&lockerDuration, "locker-duration", 10*time.Second, "The duration of the resource locker lease.")
flag.DurationVar(&lockerDuration, "locker-duration", 30*time.Second, "The duration of the resource locker lease.")
flag.DurationVar(&lockerRenewInterval, "locker-renew-interval", 5*time.Second, "The interval at which the resource locker lease is renewed.")
flag.IntVar(&provisioningHTTPPort, "provisioning-http-port", 8080, "The port on which the provisioning HTTP server listens.")
flag.BoolVar(&provisioningHTTPValidateSourceIP, "provisioning-http-validate-source-ip", false, "If set, the provisioning HTTP server will validate the source IP of incoming requests against the DeviceIPLabel of Device resources.")
Expand Down Expand Up @@ -202,8 +204,15 @@ func main() {
})
}

var watchNamespaces map[string]cache.Config
if watchNamespace != "" {
watchNamespaces = map[string]cache.Config{
watchNamespace: {},
}
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Cache: cache.Options{ReaderFailOnMissingInformer: true},
Cache: cache.Options{ReaderFailOnMissingInformer: true, DefaultNamespaces: watchNamespaces},
Controller: config.Controller{UsePriorityQueue: ptr.To(true), MaxConcurrentReconciles: maxConcurrentReconciles},
Scheme: scheme,
Metrics: metricsServerOptions,
Expand Down
8 changes: 8 additions & 0 deletions config/crd/bases/networking.metal.ironcore.dev_devices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ spec:
name: Ports
priority: 1
type: string
- jsonPath: .spec.paused
name: Paused
priority: 1
type: boolean
- jsonPath: .status.phase
name: Phase
type: string
Expand Down Expand Up @@ -178,6 +182,10 @@ spec:
x-kubernetes-validations:
- message: SecretRef is required once set
rule: '!has(oldSelf.secretRef) || has(self.secretRef)'
paused:
description: Paused can be used to prevent controllers from processing
the Device and its associated objects.
type: boolean
provisioning:
description: |-
Provisioning is an optional configuration for the device provisioning process.
Expand Down
2 changes: 1 addition & 1 deletion config/develop/manager_patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
- --leader-elect=false
- --health-probe-bind-address=:8081
- --provider=openconfig
- --requeue-interval=15s
- --requeue-interval=30s
- --max-concurrent-reconciles=5
27 changes: 27 additions & 0 deletions internal/annotations/annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
// SPDX-License-Identifier: Apache-2.0

// Package annotations implements annotation helper functions.
package annotations

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
)

// IsPaused returns true if the Device is paused or the object has the [v1alpha1.PausedAnnotation].
func IsPaused(device *v1alpha1.Device, obj metav1.Object) bool {
return (device.Spec.Paused != nil && *device.Spec.Paused) || HasPaused(obj)
}

// HasPaused returns true if the object has the [v1alpha1.PausedAnnotation].
func HasPaused(obj metav1.Object) bool {
return Has(obj, v1alpha1.PausedAnnotation)
}

// Has returns true if the object has the specified annotation.
func Has(obj metav1.Object, annotation string) bool {
_, ok := obj.GetAnnotations()[annotation]
return ok
}
6 changes: 6 additions & 0 deletions internal/controller/cisco/nx/bordergateway_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (

nxv1alpha1 "github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1"
"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
"github.com/ironcore-dev/network-operator/internal/annotations"
"github.com/ironcore-dev/network-operator/internal/conditions"
"github.com/ironcore-dev/network-operator/internal/deviceutil"
"github.com/ironcore-dev/network-operator/internal/provider"
Expand Down Expand Up @@ -103,6 +104,11 @@ func (r *BorderGatewayReconciler) Reconcile(ctx context.Context, req ctrl.Reques
return ctrl.Result{}, err
}

if annotations.IsPaused(device, obj) {
log.Info("Reconciliation is paused for this object")
return ctrl.Result{}, nil
}

if err := r.Locker.AcquireLock(ctx, device.Name, "cisco-nx-border-gateway-controller"); err != nil {
if errors.Is(err, resourcelock.ErrLockAlreadyHeld) {
log.Info("Device is already locked, requeuing reconciliation")
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/cisco/nx/system_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

nxv1alpha1 "github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1"
"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
"github.com/ironcore-dev/network-operator/internal/annotations"
"github.com/ironcore-dev/network-operator/internal/conditions"
"github.com/ironcore-dev/network-operator/internal/deviceutil"
"github.com/ironcore-dev/network-operator/internal/provider"
Expand Down Expand Up @@ -96,6 +97,11 @@ func (r *SystemReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ c
return ctrl.Result{}, err
}

if annotations.IsPaused(device, obj) {
log.Info("Reconciliation is paused for this object")
return ctrl.Result{}, nil
}

if err := r.Locker.AcquireLock(ctx, device.Name, "cisco-nx-system-controller"); err != nil {
if errors.Is(err, resourcelock.ErrLockAlreadyHeld) {
log.Info("Device is already locked, requeuing reconciliation")
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/cisco/nx/vpcdomain_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/ironcore-dev/network-operator/internal/annotations"
"github.com/ironcore-dev/network-operator/internal/conditions"
"github.com/ironcore-dev/network-operator/internal/provider"
"github.com/ironcore-dev/network-operator/internal/resourcelock"
Expand Down Expand Up @@ -112,6 +113,11 @@ func (r *VPCDomainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{}, err
}

if annotations.IsPaused(device, obj) {
log.Info("Reconciliation is paused for this object")
return ctrl.Result{}, nil
}

if err := r.Locker.AcquireLock(ctx, device.Name, "cisco-nx-vpcdomain-controller"); err != nil {
if errors.Is(err, resourcelock.ErrLockAlreadyHeld) {
log.Info("Device is already locked, requeuing reconciliation")
Expand Down
62 changes: 59 additions & 3 deletions internal/controller/core/acl_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,22 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"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/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
"github.com/ironcore-dev/network-operator/internal/annotations"
"github.com/ironcore-dev/network-operator/internal/conditions"
"github.com/ironcore-dev/network-operator/internal/deviceutil"
"github.com/ironcore-dev/network-operator/internal/provider"
Expand Down Expand Up @@ -95,6 +102,11 @@ func (r *AccessControlListReconciler) Reconcile(ctx context.Context, req ctrl.Re
return ctrl.Result{}, err
}

if annotations.IsPaused(device, obj) {
log.Info("Reconciliation is paused for this object")
return ctrl.Result{}, nil
}

if err := r.Locker.AcquireLock(ctx, device.Name, "acl-controller"); err != nil {
if errors.Is(err, resourcelock.ErrLockAlreadyHeld) {
log.Info("Device is already locked, requeuing reconciliation")
Expand Down Expand Up @@ -202,11 +214,23 @@ func (r *AccessControlListReconciler) SetupWithManager(mgr ctrl.Manager) error {
return fmt.Errorf("failed to create label selector predicate: %w", err)
}

return ctrl.NewControllerManagedBy(mgr).
bldr := ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.AccessControlList{}).
Named("accesscontrollist").
WithEventFilter(filter).
Complete(r)
WithEventFilter(filter)

for _, gvk := range v1alpha1.AccessControlListDependencies {
obj := &unstructured.Unstructured{}
obj.SetGroupVersionKind(gvk)

bldr = bldr.Watches(
obj,
handler.EnqueueRequestsFromMapFunc(r.accessControlListsForProviderConfig),
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
)
}

return bldr.Complete(r)
}

// scope holds the different objects that are read and used during the reconcile.
Expand Down Expand Up @@ -276,3 +300,35 @@ func (r *AccessControlListReconciler) finalize(ctx context.Context, s *aclScope)
ProviderConfig: s.ProviderConfig,
})
}

// accessControlListsForProviderConfig is a [handler.MapFunc] to be used to enqueue requests for reconciliation
// for a AccessControlList to update when one of its referenced provider configurations gets updated.
func (r *AccessControlListReconciler) accessControlListsForProviderConfig(ctx context.Context, obj client.Object) []reconcile.Request {
log := ctrl.LoggerFrom(ctx, "Object", klog.KObj(obj))

list := &v1alpha1.AccessControlListList{}
if err := r.List(ctx, list, client.InNamespace(obj.GetNamespace())); err != nil {
log.Error(err, "Failed to list AccessControlLists")
return nil
}

gkv := obj.GetObjectKind().GroupVersionKind()

var requests []reconcile.Request
for _, m := range list.Items {
if m.Spec.ProviderConfigRef != nil &&
m.Spec.ProviderConfigRef.Name == obj.GetName() &&
m.Spec.ProviderConfigRef.Kind == gkv.Kind &&
m.Spec.ProviderConfigRef.APIVersion == gkv.GroupVersion().Identifier() {
log.Info("Enqueuing AccessControlList for reconciliation", "AccessControlList", klog.KObj(&m))
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: m.Name,
Namespace: m.Namespace,
},
})
}
}

return requests
}
Loading