diff --git a/PROJECT b/PROJECT index 24face925..5cb5ac48c 100644 --- a/PROJECT +++ b/PROJECT @@ -289,4 +289,12 @@ resources: kind: DHCPRelay path: github.com/ironcore-dev/network-operator/api/core/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: networking.metal.ironcore.dev + kind: MacSec + path: github.com/ironcore-dev/network-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/Tiltfile b/Tiltfile index b03e5885b..9bd7c35ac 100644 --- a/Tiltfile +++ b/Tiltfile @@ -70,6 +70,10 @@ k8s_resource(new_name='acl', objects=['acl:accesscontrollist'], trigger_mode=TRI k8s_yaml('./config/samples/v1alpha1_certificate.yaml') k8s_resource(new_name='trustpoint', objects=['network-operator:issuer', 'network-operator-ca:certificate', 'trustpoint:certificate'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False) +#k8s_yaml('./config/samples/v1alpha1_macsec.yaml') +#k8s_resource(new_name='macsec-eth1-1', objects=['macsec-sample:macsec'], resource_deps=['leaf1', 'eth1-1'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False) +#k8s_resource(new_name='macsec-secrets', objects=['customer-secret:secret', 'customer-secret-rollover:secret'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False) + k8s_yaml('./config/samples/v1alpha1_snmp.yaml') k8s_resource(new_name='snmp', objects=['snmp:snmp'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False) diff --git a/api/core/v1alpha1/macsec_types.go b/api/core/v1alpha1/macsec_types.go new file mode 100644 index 000000000..c2f248ea9 --- /dev/null +++ b/api/core/v1alpha1/macsec_types.go @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// / ConfidentialityOffset represents the number of octets in an Ethernet frame that are sent in unencrypted plain-text. +type ConfidentialityOffset uint8 + +// +kubebuilder:validation:Enum=0;30;50 +const ( + // ZeroBytes represents broadcast traffic. + ZeroBytes ConfidentialityOffset = 0 + // ThirtyBytes represents multicast traffic. + ThirtyBytes ConfidentialityOffset = 30 + // FiftyBytes represents unicast traffic. + FiftyBytes ConfidentialityOffset = 50 +) + +// MacSecSpec defines the desired state of MacSec. +type MacSecSpec struct { + // DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace. + // Immutable. + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="DeviceRef is immutable" + DeviceRef LocalObjectReference `json:"deviceRef"` + + // ProviderConfigRef is a reference to a resource holding the provider-specific configuration. + // This reference is used to link the Interface to its provider-specific configuration. + // +optional + ProviderConfigRef *TypedLocalObjectReference `json:"providerConfigRef,omitempty"` + + // Name is the name of the interface. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Name is immutable" + Name string `json:"name"` + + // Description provides a human-readable description of the macsec policy. + // +optional + // +kubebuilder:validation:MaxLength=255 + Description string `json:"description,omitempty"` + + // InterfaceRef is a reference to the interface on which MACsec is enabled. The interface must exist on the Device specified by deviceRef. + // Immutable. + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="InterfaceRef is immutable" + InterfaceRef LocalObjectReference `json:"interfaceRef,omitempty"` + + // PreSharedKeyRef is a list of references to pre-shared keys used for MACSec encryption. + // +optional + PreSharedKeyRef []LocalObjectReference `json:"preSharedkeyRef,omitempty"` + + // Policy defines the MACSec policy configuration. + // +optional + Policy *MacSecPolicy `json:"policy,omitempty"` +} + +type MacSecPolicy struct { + CipherSuite string `json:"cipherSuite,omitempty"` + + // Number of octets in an Ethernet frame that are sent in unencrypted plain-text + // +optional + ConfidentialityOffset ConfidentialityOffset `json:"confidentialityOffset,omitempty"` + + // Specifies the key server priority used by the MACsec Key Agreement (MKA) protocol to select the key server when + // MACsec is enabled using static connectivity association key (CAK) security mode. + // +optional + KeyServerPriority uint8 `json:"keyServerPriority,omitempty"` + + // Number of out-of-order packets that can be received before the secure channel is considered to be inoperable. + // A value of 0 means that frames are accepted only in the correct order. + // +optional + RelayProtection uint16 `json:"relayProtection,omitempty"` +} + +// MacSecStatus defines the observed state of MacSec. +type MacSecStatus struct { + // The conditions are a list of status objects that describe the state of the MacSec. + //+listType=map + //+listMapKey=type + //+patchStrategy=merge + //+patchMergeKey=type + //+optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Device",type="string",JSONPath=".spec.deviceRef.name",description="The device this MacSec is configured on" +// +kubebuilder:printcolumn:name="Interface",type="string",JSONPath=".spec.interfaceRef.name",description="The interface this MacSec is configured on" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status of the MacSec" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// MacSec is the Schema for the macsecs API. +type MacSec struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MacSecSpec `json:"spec,omitempty"` + Status MacSecStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// MacSecList contains a list of MacSec. +type MacSecList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MacSec `json:"items"` +} + +// GetConditions implements conditions.Getter. +func (m *MacSec) GetConditions() []metav1.Condition { + return m.Status.Conditions +} + +// SetConditions implements conditions.Setter. +func (m *MacSec) SetConditions(conditions []metav1.Condition) { + m.Status.Conditions = conditions +} + +func init() { + SchemeBuilder.Register(&MacSec{}, &MacSecList{}) +} diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index 3571a97c0..217594819 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -2090,6 +2090,134 @@ func (in *LogServer) DeepCopy() *LogServer { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MacSec) DeepCopyInto(out *MacSec) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacSec. +func (in *MacSec) DeepCopy() *MacSec { + if in == nil { + return nil + } + out := new(MacSec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MacSec) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MacSecList) DeepCopyInto(out *MacSecList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MacSec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacSecList. +func (in *MacSecList) DeepCopy() *MacSecList { + if in == nil { + return nil + } + out := new(MacSecList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MacSecList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MacSecPolicy) DeepCopyInto(out *MacSecPolicy) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacSecPolicy. +func (in *MacSecPolicy) DeepCopy() *MacSecPolicy { + if in == nil { + return nil + } + out := new(MacSecPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MacSecSpec) DeepCopyInto(out *MacSecSpec) { + *out = *in + out.DeviceRef = in.DeviceRef + if in.ProviderConfigRef != nil { + in, out := &in.ProviderConfigRef, &out.ProviderConfigRef + *out = new(TypedLocalObjectReference) + **out = **in + } + out.InterfaceRef = in.InterfaceRef + if in.PreSharedKeyRef != nil { + in, out := &in.PreSharedKeyRef, &out.PreSharedKeyRef + *out = make([]LocalObjectReference, len(*in)) + copy(*out, *in) + } + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(MacSecPolicy) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacSecSpec. +func (in *MacSecSpec) DeepCopy() *MacSecSpec { + if in == nil { + return nil + } + out := new(MacSecSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MacSecStatus) DeepCopyInto(out *MacSecStatus) { + *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 MacSecStatus. +func (in *MacSecStatus) DeepCopy() *MacSecStatus { + if in == nil { + return nil + } + out := new(MacSecStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagementAccess) DeepCopyInto(out *ManagementAccess) { *out = *in diff --git a/charts/network-operator/templates/rbac/manager-role.yaml b/charts/network-operator/templates/rbac/manager-role.yaml index 580e6b8bd..55f0b65f7 100644 --- a/charts/network-operator/templates/rbac/manager-role.yaml +++ b/charts/network-operator/templates/rbac/manager-role.yaml @@ -27,6 +27,14 @@ rules: - list - update - watch +- apiGroups: + - "" + - events.k8s.io + resources: + - events + verbs: + - create + - patch - apiGroups: - coordination.k8s.io resources: @@ -38,13 +46,6 @@ rules: - list - update - watch -- apiGroups: - - events.k8s.io - resources: - - events - verbs: - - create - - patch - apiGroups: - networking.metal.ironcore.dev resources: @@ -60,6 +61,7 @@ rules: - interfaces - isis - lldps + - macsecs - managementaccesses - networkvirtualizationedges - ntp @@ -95,6 +97,7 @@ rules: - interfaces/finalizers - isis/finalizers - lldps/finalizers + - macsecs/finalizers - managementaccesses/finalizers - networkvirtualizationedges/finalizers - ntp/finalizers @@ -124,6 +127,7 @@ rules: - interfaces/status - isis/status - lldps/status + - macsecs/status - managementaccesses/status - networkvirtualizationedges/status - ntp/status diff --git a/cmd/main.go b/cmd/main.go index 82de236ce..d5a0042bd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -625,6 +625,18 @@ func main() { //nolint:gocyclo os.Exit(1) } + if err := (&corecontroller.MacSecReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorder("macsec-controller"), + Provider: prov, + Locker: locker, + RequeueInterval: requeueInterval, + }).SetupWithManager(ctx, mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "MacSec") + os.Exit(1) + } + if os.Getenv("ENABLE_WEBHOOKS") != "false" { if err := webhookv1alpha1.SetupVRFWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "VRF") diff --git a/config/crd/bases/networking.metal.ironcore.dev_macsecs.yaml b/config/crd/bases/networking.metal.ironcore.dev_macsecs.yaml new file mode 100644 index 000000000..9bcb9f1d9 --- /dev/null +++ b/config/crd/bases/networking.metal.ironcore.dev_macsecs.yaml @@ -0,0 +1,257 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: macsecs.networking.metal.ironcore.dev +spec: + group: networking.metal.ironcore.dev + names: + kind: MacSec + listKind: MacSecList + plural: macsecs + singular: macsec + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The device this MacSec is configured on + jsonPath: .spec.deviceRef.name + name: Device + type: string + - description: The interface this MacSec is configured on + jsonPath: .spec.interfaceRef.name + name: Interface + type: string + - description: Ready status of the MacSec + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: MacSec is the Schema for the macsecs API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: MacSecSpec defines the desired state of MacSec. + properties: + description: + description: Description provides a human-readable description of + the macsec policy. + maxLength: 255 + type: string + deviceRef: + description: |- + DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace. + Immutable. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + maxLength: 63 + minLength: 1 + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: DeviceRef is immutable + rule: self == oldSelf + interfaceRef: + description: |- + InterfaceRef is a reference to the interface on which MACsec is enabled. The interface must exist on the Device specified by deviceRef. + Immutable. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + maxLength: 63 + minLength: 1 + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: InterfaceRef is immutable + rule: self == oldSelf + name: + description: Name is the name of the interface. + maxLength: 255 + minLength: 1 + type: string + x-kubernetes-validations: + - message: Name is immutable + rule: self == oldSelf + policy: + description: Policy defines the MACSec policy configuration. + properties: + cipherSuite: + type: string + confidentialityOffset: + description: Number of octets in an Ethernet frame that are sent + in unencrypted plain-text + type: integer + keyServerPriority: + description: |- + Specifies the key server priority used by the MACsec Key Agreement (MKA) protocol to select the key server when + MACsec is enabled using static connectivity association key (CAK) security mode. + type: integer + relayProtection: + description: |- + Number of out-of-order packets that can be received before the secure channel is considered to be inoperable. + A value of 0 means that frames are accepted only in the correct order. + type: integer + type: object + preSharedkeyRef: + description: PreSharedKeyRef is a list of references to pre-shared + keys used for MACSec encryption. + items: + description: |- + LocalObjectReference contains enough information to locate a + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + maxLength: 63 + minLength: 1 + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic + type: array + providerConfigRef: + description: |- + ProviderConfigRef is a reference to a resource holding the provider-specific configuration. + This reference is used to link the Interface to its provider-specific configuration. + properties: + apiVersion: + description: APIVersion is the api group version of the resource + being referenced. + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/)?([a-z0-9]([-a-z0-9]*[a-z0-9])?)$ + type: string + kind: + description: |- + Kind of the resource being referenced. + Kind must consist of alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name of the resource being referenced. + Name must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - apiVersion + - kind + - name + type: object + x-kubernetes-map-type: atomic + required: + - deviceRef + - interfaceRef + - name + type: object + status: + description: MacSecStatus defines the observed state of MacSec. + properties: + conditions: + description: The conditions are a list of status objects that describe + the state of the MacSec. + 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 + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 182f057f4..d1311bd53 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -34,6 +34,7 @@ resources: - bases/nx.cisco.networking.metal.ironcore.dev_interfaceconfigs.yaml - bases/nx.cisco.networking.metal.ironcore.dev_lldpconfigs.yaml - bases/nx.cisco.networking.metal.ironcore.dev_bgpconfigs.yaml +- bases/networking.metal.ironcore.dev_macsecs.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: [] diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index f6aab1b99..3b2fbf04e 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -22,6 +22,9 @@ resources: # default, aiding admins in cluster management. Those roles are # not used by the network-operator itself. You can comment the following lines # if you do not want those helpers be installed with your Project. +- macsec_admin_role.yaml +- macsec_editor_role.yaml +- macsec_viewer_role.yaml - accesscontrollist_admin_role.yaml - accesscontrollist_editor_role.yaml - accesscontrollist_viewer_role.yaml diff --git a/config/rbac/macsec_admin_role.yaml b/config/rbac/macsec_admin_role.yaml new file mode 100644 index 000000000..81f6da500 --- /dev/null +++ b/config/rbac/macsec_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project network-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over networking.metal.ironcore.dev. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: network-operator + app.kubernetes.io/managed-by: kustomize + name: macsec-admin-role +rules: +- apiGroups: + - networking.metal.ironcore.dev + resources: + - macsecs + verbs: + - '*' +- apiGroups: + - networking.metal.ironcore.dev + resources: + - macsecs/status + verbs: + - get diff --git a/config/rbac/macsec_editor_role.yaml b/config/rbac/macsec_editor_role.yaml new file mode 100644 index 000000000..966f3dd39 --- /dev/null +++ b/config/rbac/macsec_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project network-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the networking.metal.ironcore.dev. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: network-operator + app.kubernetes.io/managed-by: kustomize + name: macsec-editor-role +rules: +- apiGroups: + - networking.metal.ironcore.dev + resources: + - macsecs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.metal.ironcore.dev + resources: + - macsecs/status + verbs: + - get diff --git a/config/rbac/macsec_viewer_role.yaml b/config/rbac/macsec_viewer_role.yaml new file mode 100644 index 000000000..1c8ccb06f --- /dev/null +++ b/config/rbac/macsec_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project network-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to networking.metal.ironcore.dev resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: network-operator + app.kubernetes.io/managed-by: kustomize + name: macsec-viewer-role +rules: +- apiGroups: + - networking.metal.ironcore.dev + resources: + - macsecs + verbs: + - get + - list + - watch +- apiGroups: + - networking.metal.ironcore.dev + resources: + - macsecs/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index e9757ff33..fbf876e6b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -21,6 +21,14 @@ rules: - list - update - watch +- apiGroups: + - "" + - events.k8s.io + resources: + - events + verbs: + - create + - patch - apiGroups: - coordination.k8s.io resources: @@ -32,13 +40,6 @@ rules: - list - update - watch -- apiGroups: - - events.k8s.io - resources: - - events - verbs: - - create - - patch - apiGroups: - networking.metal.ironcore.dev resources: @@ -54,6 +55,7 @@ rules: - interfaces - isis - lldps + - macsecs - managementaccesses - networkvirtualizationedges - ntp @@ -89,6 +91,7 @@ rules: - interfaces/finalizers - isis/finalizers - lldps/finalizers + - macsecs/finalizers - managementaccesses/finalizers - networkvirtualizationedges/finalizers - ntp/finalizers @@ -118,6 +121,7 @@ rules: - interfaces/status - isis/status - lldps/status + - macsecs/status - managementaccesses/status - networkvirtualizationedges/status - ntp/status diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index ac22ba582..240c38591 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -24,6 +24,7 @@ resources: - v1alpha1_nve.yaml - v1alpha1_prefixset.yaml - v1alpha1_routingpolicy.yaml +- v1alpha1_macsec.yaml - cisco/nx/v1alpha1_bordergateway.yaml - cisco/nx/v1alpha1_managementaccessconfig.yaml - cisco/nx/v1alpha1_nveconfig.yaml diff --git a/config/samples/v1alpha1_macsec.yaml b/config/samples/v1alpha1_macsec.yaml new file mode 100644 index 000000000..94e2c15f7 --- /dev/null +++ b/config/samples/v1alpha1_macsec.yaml @@ -0,0 +1,37 @@ + apiVersion: networking.metal.ironcore.dev/v1alpha1 + kind: MacSec + metadata: + labels: + app.kubernetes.io/name: network-operator + app.kubernetes.io/managed-by: kustomize + networking.metal.ironcore.dev/device-name: leaf1 + name: macsec-sample + spec: + deviceRef: + name: leaf1 + interfaceRef: + name: eth1-1 + name: macsec-eth1-1 + description: "MACSec configuration for eth1/1 interface" + preSharedkeyRef: + - name: customer-secret + policy: + cipherSuite: gcm-aes-128 + confidentialityOffset: 30 + keyServerPriority: 0 + relayProtection: 0 +--- +apiVersion: v1 +kind: Secret +metadata: + name: customer-secret + labels: + app.kubernetes.io/name: network-operator + app.kubernetes.io/managed-by: kustomize +type: Opaque +data: + key: Y3VzdG9tZXItc2VjcmV0LWtleQ== # preshared key +stringdata: + keyId: "1234" + lifetime: "3600" + algorithm: "aes-128-cmac" diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index b333d37e9..1dc81fdde 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -26,6 +26,7 @@ SPDX-License-Identifier: Apache-2.0 - [ISIS](#isis) - [Interface](#interface) - [LLDP](#lldp) +- [MacSec](#macsec) - [ManagementAccess](#managementaccess) - [NTP](#ntp) - [NetworkVirtualizationEdge](#networkvirtualizationedge) @@ -804,6 +805,19 @@ _Appears in:_ | `MD5` | | +#### ConfidentialityOffset + +_Underlying type:_ _integer_ + +/ ConfidentialityOffset represents the number of octets in an Ethernet frame that are sent in unencrypted plain-text. + + + +_Appears in:_ +- [MacSecPolicy](#macsecpolicy) + + + #### ConfigMapKeySelector @@ -1677,6 +1691,7 @@ _Appears in:_ - [KeepAlive](#keepalive) - [LLDPInterface](#lldpinterface) - [LLDPSpec](#lldpspec) +- [MacSecSpec](#macsecspec) - [ManagementAccessSpec](#managementaccessspec) - [NTPSpec](#ntpspec) - [NetworkVirtualizationEdgeSpec](#networkvirtualizationedgespec) @@ -1739,6 +1754,81 @@ _Appears in:_ | `port` _integer_ | The destination port number for syslog UDP messages to
the server. The default is 514. | 514 | Optional: \{\}
| +#### MacSec + + + +MacSec is the Schema for the macsecs API. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `networking.metal.ironcore.dev/v1alpha1` | | | +| `kind` _string_ | `MacSec` | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[MacSecSpec](#macsecspec)_ | | | | +| `status` _[MacSecStatus](#macsecstatus)_ | | | | + + +#### MacSecPolicy + + + + + + + +_Appears in:_ +- [MacSecSpec](#macsecspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `cipherSuite` _string_ | | | | +| `confidentialityOffset` _[ConfidentialityOffset](#confidentialityoffset)_ | Number of octets in an Ethernet frame that are sent in unencrypted plain-text | | Optional: \{\}
| +| `keyServerPriority` _integer_ | Specifies the key server priority used by the MACsec Key Agreement (MKA) protocol to select the key server when
MACsec is enabled using static connectivity association key (CAK) security mode. | | Optional: \{\}
| +| `relayProtection` _integer_ | Number of out-of-order packets that can be received before the secure channel is considered to be inoperable.
A value of 0 means that frames are accepted only in the correct order. | | Optional: \{\}
| + + +#### MacSecSpec + + + +MacSecSpec defines the desired state of MacSec. + + + +_Appears in:_ +- [MacSec](#macsec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `deviceRef` _[LocalObjectReference](#localobjectreference)_ | DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace.
Immutable. | | Required: \{\}
| +| `interfaceRef` _[LocalObjectReference](#localobjectreference)_ | InterfaceRef is a reference to the interface on which MACsec is enabled. The interface must exist on the Device specified by deviceRef.
Immutable. | | Required: \{\}
| +| `name` _string_ | Name is the name of the interface. | | MaxLength: 255
MinLength: 1
Required: \{\}
| +| `description` _string_ | Description provides a human-readable description of the macsec policy. | | MaxLength: 255
Optional: \{\}
| +| `preSharedkeyRef` _[LocalObjectReference](#localobjectreference) array_ | PreSharedKeyRef is a list of references to pre-shared keys used for MACSec encryption. | | Optional: \{\}
| +| `policy` _[MacSecPolicy](#macsecpolicy)_ | Policy defines the MACSec policy configuration. | | Optional: \{\}
| + + +#### MacSecStatus + + + +MacSecStatus defines the observed state of MacSec. + + + +_Appears in:_ +- [MacSec](#macsec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array_ | The conditions are a list of status objects that describe the state of the MacSec. | | Optional: \{\}
| + + #### ManagementAccess diff --git a/internal/controller/core/macsec_controller.go b/internal/controller/core/macsec_controller.go new file mode 100644 index 000000000..418bee772 --- /dev/null +++ b/internal/controller/core/macsec_controller.go @@ -0,0 +1,436 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + "context" + "errors" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + + "k8s.io/apimachinery/pkg/api/equality" + 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/runtime" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/tools/events" + 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/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/conditions" + "github.com/ironcore-dev/network-operator/internal/deviceutil" + "github.com/ironcore-dev/network-operator/internal/paused" + "github.com/ironcore-dev/network-operator/internal/provider" + "github.com/ironcore-dev/network-operator/internal/resourcelock" +) + +// MacSecReconciler reconciles a MacSec object +type MacSecReconciler struct { + client.Client + Scheme *runtime.Scheme + + // WatchFilterValue is the label value used to filter events prior to reconciliation. + WatchFilterValue string + + // Recorder is used to record events for the controller. + // More info: https://book.kubebuilder.io/reference/raising-events + Recorder events.EventRecorder + + // Provider is the driver that will be used to create & delete the macsec. + Provider provider.ProviderFunc + + // Locker is used to synchronize operations on resources targeting the same device. + Locker *resourcelock.ResourceLocker + + // RequeueInterval is the duration after which the controller should requeue the reconciliation, + // regardless of changes. + RequeueInterval time.Duration +} + +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=macsecs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=macsecs/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=macsecs/finalizers,verbs=update +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=devices,verbs=get;list;watch +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=interfaces,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/reconcile +func (r *MacSecReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + log := ctrl.LoggerFrom(ctx) + log.Info("Reconciling resource") + + obj := new(v1alpha1.MacSec) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + // If the custom resource is not found then it usually means that it was deleted or not created + // In this way, we will stop the reconciliation + log.Info("Resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get resource") + return ctrl.Result{}, err + } + + prov, ok := r.Provider().(provider.MacSecProvider) + if !ok { + if meta.SetStatusCondition(&obj.Status.Conditions, metav1.Condition{ + Type: v1alpha1.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: v1alpha1.NotImplementedReason, + Message: "Provider does not implement provider.MacSecProvider", + }) { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + return ctrl.Result{}, nil + } + + device, err := deviceutil.GetDeviceByName(ctx, r, obj.Namespace, obj.Spec.DeviceRef.Name) + if err != nil { + return ctrl.Result{}, err + } + + if isPaused, requeue, err := paused.EnsureCondition(ctx, r.Client, device, obj); isPaused || requeue || err != nil { + return ctrl.Result{Requeue: requeue}, err + } + + if err := r.Locker.AcquireLock(ctx, device.Name, "macsec-controller"); err != nil { + if errors.Is(err, resourcelock.ErrLockAlreadyHeld) { + log.Info("Device is already locked, requeuing reconciliation") + return ctrl.Result{RequeueAfter: Jitter(time.Second), Priority: new(LockWaitPriorityHigh)}, nil + } + log.Error(err, "Failed to acquire device lock") + return ctrl.Result{}, err + } + defer func() { + if err := r.Locker.ReleaseLock(ctx, device.Name, "macsec-controller"); err != nil { + log.Error(err, "Failed to release device lock") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + conn, err := deviceutil.GetDeviceConnection(ctx, r, device) + if err != nil { + return ctrl.Result{}, err + } + + var cfg *provider.ProviderConfig + if obj.Spec.ProviderConfigRef != nil { + cfg, err = provider.GetProviderConfig(ctx, r, obj.Namespace, obj.Spec.ProviderConfigRef) + if err != nil { + return ctrl.Result{}, err + } + } + + // Validate that the referenced interface exists + intf, err := GetInterfaceByName(ctx, r, obj.Namespace, obj.Spec.InterfaceRef.Name) + if err != nil { + return ctrl.Result{}, err + } + + // Validate that all pre-shared key secrets exist + secrets, err := r.validatePreSharedKeySecrets(ctx, obj) + if err != nil { + log.Error(err, "Pre-shared key validation failed") + if meta.SetStatusCondition(&obj.Status.Conditions, metav1.Condition{ + Type: v1alpha1.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: v1alpha1.ErrorReason, + Message: fmt.Sprintf("Pre-shared key validation failed: %v", err), + }) { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + return ctrl.Result{}, nil + } + + s := &macSecScope{ + Device: device, + MacSec: obj, + Interface: intf, + Secrets: secrets, + Connection: conn, + ProviderConfig: cfg, + Provider: prov, + } + + if !obj.DeletionTimestamp.IsZero() { + if controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { + if err := r.finalize(ctx, s); err != nil { + log.Error(err, "Failed to finalize MacSec resource") + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(obj, v1alpha1.FinalizerName) + if err := r.Update(ctx, obj); err != nil { + log.Error(err, "Failed to remove finalizer from MacSec resource") + return ctrl.Result{}, err + } + } + log.Info("Resource is being deleted, skipping reconciliation") + return ctrl.Result{}, nil + } + + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers + if !controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { + controllerutil.AddFinalizer(obj, v1alpha1.FinalizerName) + if err := r.Update(ctx, obj); err != nil { + log.Error(err, "Failed to add finalizer to resource") + return ctrl.Result{}, err + } + log.Info("Added finalizer to resource") + return ctrl.Result{}, nil + } + + orig := obj.DeepCopy() + if conditions.InitializeConditions(obj, v1alpha1.ReadyCondition, v1alpha1.ConfiguredCondition, v1alpha1.OperationalCondition) { + log.Info("Initializing status conditions") + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + + // Always attempt to update the metadata/status after reconciliation + defer func() { + if !equality.Semantic.DeepEqual(orig.ObjectMeta, obj.ObjectMeta) { + // Pass obj.DeepCopy() to avoid Patch() modifying obj and interfering with status update below + if err := r.Patch(ctx, obj.DeepCopy(), client.MergeFrom(orig)); err != nil { + log.Error(err, "Failed to update resource metadata") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + } + if !equality.Semantic.DeepEqual(orig.Status, obj.Status) { + if err := r.Status().Patch(ctx, obj, client.MergeFrom(orig)); err != nil { + log.Error(err, "Failed to update status") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + } + }() + + if err := r.reconcile(ctx, s); err != nil { + log.Error(err, "Failed to reconcile resource") + return ctrl.Result{}, err + } + + return ctrl.Result{RequeueAfter: Jitter(r.RequeueInterval)}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *MacSecReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { + labelSelector := metav1.LabelSelector{} + if r.WatchFilterValue != "" { + labelSelector.MatchLabels = map[string]string{v1alpha1.WatchLabel: r.WatchFilterValue} + } + + filter, err := predicate.LabelSelectorPredicate(labelSelector) + if err != nil { + return fmt.Errorf("failed to create label selector predicate: %w", err) + } + + if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.MacSec{}, v1alpha1.DeviceRefIndexKey, func(obj client.Object) []string { + m := obj.(*v1alpha1.MacSec) + return []string{m.Spec.DeviceRef.Name} + }); err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.MacSec{}). + Named("macsec"). + WithEventFilter(filter). + // Watches enqueues MacSecs for referenced Secret resources. + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.secretToMacSec), + builder.WithPredicates(predicate.GenerationChangedPredicate{}), + ). + // Watches enqueues MacSecs for updates in referenced Device resources. + // Triggers on create, delete, and update events when the device's effective pause state changes. + Watches( + &v1alpha1.Device{}, + handler.EnqueueRequestsFromMapFunc(r.deviceToMacSecs), + builder.WithPredicates(predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + return paused.DevicePausedChanged(e.ObjectOld, e.ObjectNew) + }, + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + }), + ). + Complete(r) +} + +// scope holds the different objects that are read and used during the reconcile. +type macSecScope struct { + Device *v1alpha1.Device + MacSec *v1alpha1.MacSec + Connection *deviceutil.Connection + ProviderConfig *provider.ProviderConfig + Interface *v1alpha1.Interface + Secrets []corev1.Secret + Provider provider.MacSecProvider +} + +func (r *MacSecReconciler) reconcile(ctx context.Context, s *macSecScope) (reterr error) { + if s.MacSec.Labels == nil { + s.MacSec.Labels = make(map[string]string) + } + + s.MacSec.Labels[v1alpha1.DeviceLabel] = s.Device.Name + + // Ensure the MacSec is owned by the Device. + if !controllerutil.HasControllerReference(s.MacSec) { + if err := controllerutil.SetOwnerReference(s.Device, s.MacSec, r.Scheme, controllerutil.WithBlockOwnerDeletion(true)); err != nil { + return err + } + } + + defer func() { + conditions.RecomputeReady(s.MacSec) + }() + + if err := s.Provider.Connect(ctx, s.Connection); err != nil { + return fmt.Errorf("failed to connect to provider: %w", err) + } + defer func() { + if err := s.Provider.Disconnect(ctx, s.Connection); err != nil { + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + // Ensure the MacSec is realized on the provider. + err := s.Provider.EnsureMacSec(ctx, &provider.EnsureMacSecRequest{ + MacSec: s.MacSec, + Secrets: s.Secrets, + }) + + cond := conditions.FromError(err) + conditions.Set(s.MacSec, cond) + + return nil +} + +func (r *MacSecReconciler) finalize(ctx context.Context, s *macSecScope) (reterr error) { + if err := s.Provider.Connect(ctx, s.Connection); err != nil { + return fmt.Errorf("failed to connect to provider: %w", err) + } + defer func() { + if err := s.Provider.Disconnect(ctx, s.Connection); err != nil { + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + return s.Provider.DeleteMacSec(ctx, &provider.DeleteMacSecRequest{ + MacSec: s.MacSec, + }) +} + +// validatePreSharedKeySecrets validates that all pre-shared key secrets referenced in the MacSec spec exist +func (r *MacSecReconciler) validatePreSharedKeySecrets(ctx context.Context, macSec *v1alpha1.MacSec) ([]corev1.Secret, error) { + secrets := []corev1.Secret{} + for _, psk := range macSec.Spec.PreSharedKeyRef { + secret := new(corev1.Secret) + if err := r.Get(ctx, client.ObjectKey{ + Namespace: macSec.Namespace, + Name: psk.Name, + }, secret); err != nil { + return nil, fmt.Errorf("pre-shared key secret not found: %s", psk.Name) + } + secrets = append(secrets, *secret) + for _, key := range []string{"lifetime", "connectivityKeyName", "algorithm"} { + _, ok := secret.Data[key] + if !ok { + return nil, fmt.Errorf("pre-shared key secret %s does not contain a '%s' field", psk.Name, key) + } + } + } + return secrets, nil +} + +func GetInterfaceByName(ctx context.Context, r client.Reader, namespace, name string) (*v1alpha1.Interface, error) { + obj := new(v1alpha1.Interface) + if err := r.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, obj); err != nil { + return nil, fmt.Errorf("failed to get %s/%s: %w", v1alpha1.GroupVersion.WithKind("Interface").String(), name, err) + } + return obj, nil +} + +// secretToMacSec is a [handler.MapFunc] to be used to enqueue requests for reconciliation +// for a MacSec to update when one of its referenced Secrets gets updated. +func (r *MacSecReconciler) secretToMacSec(ctx context.Context, obj client.Object) []ctrl.Request { + secret, ok := obj.(*corev1.Secret) + if !ok { + panic(fmt.Sprintf("Expected a Secret but got a %T", obj)) + } + + log := ctrl.LoggerFrom(ctx) + + macsecs := new(v1alpha1.MacSecList) + if err := r.List(ctx, macsecs); err != nil { + log.Error(err, "Failed to list MacSecs") + return nil + } + + requests := []ctrl.Request{} + for _, macsec := range macsecs.Items { + // Check if this secret is referenced by any of the pre-shared key references + for _, psk := range macsec.Spec.PreSharedKeyRef { + if psk.Name == secret.Name && macsec.Namespace == secret.Namespace { + log.Info("Enqueuing MacSec for reconciliation") + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: macsec.Name, + Namespace: macsec.Namespace, + }, + }) + } + } + } + + return requests +} + +// deviceToMacSecs is a [handler.MapFunc] to be used to enqueue requests for reconciliation +// for MacSecs when their referenced Device's effective pause state changes. +func (r *MacSecReconciler) deviceToMacSecs(ctx context.Context, obj client.Object) []ctrl.Request { + device, ok := obj.(*v1alpha1.Device) + if !ok { + panic(fmt.Sprintf("Expected a Device but got a %T", obj)) + } + + log := ctrl.LoggerFrom(ctx) + + macsecs := new(v1alpha1.MacSecList) + if err := r.List( + ctx, macsecs, + client.InNamespace(device.Namespace), + client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, + ); err != nil { + log.Error(err, "Failed to list MacSecs") + return nil + } + + requests := make([]ctrl.Request, 0, len(macsecs.Items)) + for _, m := range macsecs.Items { + log.V(2).Info("Enqueuing MacSec for reconciliation", "MacSec", m.Name) + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: m.Name, + Namespace: m.Namespace, + }, + }) + } + + return requests +} diff --git a/internal/controller/core/macsec_controller_test.go b/internal/controller/core/macsec_controller_test.go new file mode 100644 index 000000000..a9ff88511 --- /dev/null +++ b/internal/controller/core/macsec_controller_test.go @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/ironcore-dev/network-operator/internal/provider" + "github.com/ironcore-dev/network-operator/internal/resourcelock" +) + +var _ = Describe("MacSec Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "macsec-test" + const deviceName = "macsec-device" + const intName = "macsec-interface" + + ctx = context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: metav1.NamespaceDefault, + } + + macSecSecKey := types.NamespacedName{ + Name: resourceName, + Namespace: metav1.NamespaceDefault, + } + secretKey := client.ObjectKey{Name: deviceName, Namespace: metav1.NamespaceDefault} + deviceKey := client.ObjectKey{Name: deviceName, Namespace: metav1.NamespaceDefault} + intKey := client.ObjectKey{Name: intName, Namespace: metav1.NamespaceDefault} + + BeforeEach(func() { + By("Creating the endpoint credentials as a Secret") + deviceCreds := &corev1.Secret{} + if err := k8sClient.Get(ctx, secretKey, deviceCreds); errors.IsNotFound(err) { + resource := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: deviceName, + Namespace: metav1.NamespaceDefault, + }, + Data: map[string][]byte{ + corev1.BasicAuthUsernameKey: []byte("user"), + corev1.BasicAuthPasswordKey: []byte("password"), + }, + Type: corev1.SecretTypeBasicAuth, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + + By("Creating the device that will be referenced by the MacSec resource") + device := &v1alpha1.Device{ + ObjectMeta: metav1.ObjectMeta{ + Name: deviceName, + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.DeviceSpec{ + Endpoint: v1alpha1.Endpoint{ + Address: "192.168.10.2:9339", + SecretRef: &v1alpha1.SecretReference{ + Name: deviceName, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, device)).To(Succeed()) + + By("Creating an Interface resource that will be referenced by the MacSec resource") + intf := &v1alpha1.Interface{ + ObjectMeta: metav1.ObjectMeta{ + Name: intName, + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.InterfaceSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, + Name: intName, + AdminState: v1alpha1.AdminStateUp, + Description: "Test", + MTU: 9000, + Type: v1alpha1.InterfaceTypePhysical, + }, + } + Expect(k8sClient.Create(ctx, intf)).To(Succeed()) + + By("Creating the secret that will be referenced by the MacSec resource") + macsecSecret := &corev1.Secret{} + if err := k8sClient.Get(ctx, macSecSecKey, macsecSecret); err != nil && errors.IsNotFound(err) { + resource := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: metav1.NamespaceDefault, + }, + StringData: map[string]string{ + "lifetime": "3600", + "keyId": "someID", + "algorithm": "aes-256", + }, + Type: corev1.SecretTypeOpaque, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + resource := &v1alpha1.MacSec{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance MacSec") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + + By("Cleanup the specific resource instance MacSec") + + By("Cleanup the specific resource instance Interface") + intf := &v1alpha1.Interface{} + err = k8sClient.Get(ctx, intKey, intf) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, intf)).To(Succeed()) + + By("Cleanup the specific resource instance Device and Secret") + device := &v1alpha1.Device{} + err = k8sClient.Get(ctx, deviceKey, device) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, device)).To(Succeed()) + + secret := &corev1.Secret{} + err = k8sClient.Get(ctx, deviceKey, secret) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + }) + + It("should successfully reconcile the resource", func() { + By("Creating the custom resource for the Kind MacSec") + macSec := &v1alpha1.MacSec{} + err := k8sClient.Get(ctx, typeNamespacedName, macSec) + if err != nil && errors.IsNotFound(err) { + macSec = &v1alpha1.MacSec{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: v1alpha1.MacSecSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName}, + InterfaceRef: v1alpha1.LocalObjectReference{Name: intName}, + Name: "test-macsec", + Description: "Test MacSec resource", + PreSharedKeyRef: []v1alpha1.LocalObjectReference{ + { + Name: "macsec-test", + }, + }, + Policy: &v1alpha1.MacSecPolicy{ + CipherSuite: "GCM-AES-256", + }, + }, + } + } + Expect(k8sClient.Create(ctx, macSec)).To(Succeed()) + By("Reconciling Kind MacSec") + locker, err := resourcelock.NewResourceLocker( + k8sManager.GetClient(), metav1.NamespaceDefault, + 15*time.Second, + 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + controllerReconciler := &MacSecReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + Provider: func() provider.Provider { return testProvider }, + Locker: locker, + } + + // Add Finalizer + //nolint:errcheck + controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + // Add status condition + //nolint:errcheck + controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + // reconcile & update metadata + //nolint:errcheck + controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + // update status + //nolint:errcheck + controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + + // update macsec to compare status + Expect(k8sClient.Get(ctx, typeNamespacedName, macSec)).To(Succeed()) + Expect(macSec.Status.Conditions).To(HaveLen(1)) + Expect(macSec.Status.Conditions[0].Type).To(Equal(v1alpha1.ReadyCondition)) + Expect(macSec.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue)) + }) + }) +}) diff --git a/internal/controller/core/suite_test.go b/internal/controller/core/suite_test.go index 0244c354c..acb195ad1 100644 --- a/internal/controller/core/suite_test.go +++ b/internal/controller/core/suite_test.go @@ -409,6 +409,7 @@ var ( _ provider.NVEProvider = (*Provider)(nil) _ provider.LLDPProvider = (*Provider)(nil) _ provider.DHCPRelayProvider = (*Provider)(nil) + _ provider.MacSecProvider = (*Provider)(nil) ) // Provider is a simple in-memory provider for testing purposes only. @@ -445,6 +446,7 @@ type Provider struct { LLDPOperStatus bool LLDPNeighbors map[string]*provider.LLDPAdjacency DHCPRelay *v1alpha1.DHCPRelay + MacSec sets.Set[string] } func NewProvider() *Provider { @@ -464,6 +466,7 @@ func NewProvider() *Provider { RoutingPolicies: sets.New[string](), LLDPOperStatus: true, LLDPNeighbors: make(map[string]*provider.LLDPAdjacency), + MacSec: sets.New[string](), } } @@ -964,3 +967,25 @@ func (p *Provider) SetLLDPNeighbor(interfaceName, sysName, chassisID, portID str TTL: time.Duration(ttl) * time.Second, } } + +func (p *Provider) EnsureMacSec(_ context.Context, req *provider.EnsureMacSecRequest) error { + p.Lock() + defer p.Unlock() + p.MacSec.Insert(req.MacSec.Spec.Name) + return nil +} + +func (p *Provider) DeleteMacSec(_ context.Context, req *provider.DeleteMacSecRequest) error { + p.Lock() + defer p.Unlock() + p.MacSec.Delete(req.MacSec.Spec.Name) + return nil +} + +func (p *Provider) GetMacSecStatus(_ context.Context, req *provider.EnsureMacSecRequest) (provider.MacSecStatus, error) { + p.Lock() + defer p.Unlock() + return provider.MacSecStatus{ + OverallStatus: true, + }, nil +} diff --git a/internal/provider/cisco/iosxr/macsec.go b/internal/provider/cisco/iosxr/macsec.go new file mode 100644 index 000000000..be71670ce --- /dev/null +++ b/internal/provider/cisco/iosxr/macsec.go @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package iosxr + +import ( + "fmt" + "strings" + "time" + + "github.com/ironcore-dev/network-operator/internal/transport/gnmiext" +) + +var ( + _ gnmiext.DataElement = (*MacSecPolicy)(nil) + _ gnmiext.DataElement = (*KeyChain)(nil) + _ gnmiext.DataElement = (*KeyChainOperData)(nil) +) + +const ConfOffsetPrefix = "CONF-OFFSET-%d" + +type CipherSuite string + +const ( + CipherSuiteGcmAes256 CipherSuite = "GCM-AES-256" + CipherSuiteGcmAes128 CipherSuite = "GCM-AES-128" + CipherSuiteGcmAesXpn256 CipherSuite = "GCM-AES-XPN-256" + CipherSuiteGcmAesXpn128 CipherSuite = "GCM-AES-XPN-128" +) + +type CryptographicAlgorithm string + +const ( + CryptographicAlgorithmMd5 CryptographicAlgorithm = "md5" + CryptographicAlgorithmSha1 CryptographicAlgorithm = "sha-1" + CryptographicAlgorithmHmacMd5 CryptographicAlgorithm = "hmac-md5" + CryptographicAlgorithmHmacSha1_20 CryptographicAlgorithm = "hmac-sha1-20" + CryptographicAlgorithmHmacSha1_12 CryptographicAlgorithm = "hmac-sha1-12" + CryptographicAlgorithmHmacSha1_96 CryptographicAlgorithm = "hmac-sha1-96" + CryptographicAlgorithmHmacSha256 CryptographicAlgorithm = "hmac-sha-256" + CryptographicAlgorithmAes128Cmac96 CryptographicAlgorithm = "aes-128-cmac-96" +) + +type MacSecPolicy struct { + Name string `json:"-"` + CipherSuite CipherSuite `json:"cipher-suite,omitzero"` + ConfOffset string `json:"conf-offset,omitzero"` + KeyServerPriority uint8 `json:"key-server-priority,omitzero"` + RelayProtection uint16 `json:"window-size,omitzero"` +} + +func (m *MacSecPolicy) XPath() string { + return "Cisco-IOS-XR-um-macsec-cfg:macsec-policy/policy-names/policy-name[policy-name=" + m.Name + "]" +} + +func ExtractCipherSuite(cipherSuite string) (CipherSuite, error) { + switch cipherSuite { + case "GCM-AES-256": + return CipherSuiteGcmAes256, nil + case "GCM-AES-128": + return CipherSuiteGcmAes128, nil + case "GCM-AES-XPN-256": + return CipherSuiteGcmAesXpn256, nil + case "GCM-AES-XPN-128": + return CipherSuiteGcmAesXpn128, nil + default: + return "", fmt.Errorf("unsupported cipher suite: %s", cipherSuite) + } +} + +func ExtractCryptographicAlgorithm(cipherSuite string) (string, error) { + switch cipherSuite { + case "md5": + return string(CryptographicAlgorithmMd5), nil + case "sha-1": + return string(CryptographicAlgorithmSha1), nil + case "hmac-md5": + return string(CryptographicAlgorithmHmacMd5), nil + case "hmac-sha1-20": + return string(CryptographicAlgorithmHmacSha1_20), nil + case "hmac-sha1-12": + return string(CryptographicAlgorithmHmacSha1_12), nil + case "hmac-sha1-96": + return string(CryptographicAlgorithmHmacSha1_96), nil + case "hmac-sha-256": + return string(CryptographicAlgorithmHmacSha256), nil + case "aes-128-cmac-96": + return string(CryptographicAlgorithmAes128Cmac96), nil + default: + return "", fmt.Errorf("unsupported cryptographic algorithm: %s", cipherSuite) + } +} + +type KeyChain struct { + Name string `json:"-"` + Keys Keys `json:"keys,omitzero"` +} + +type Keys struct { + Key []Key `json:"key,omitzero"` +} + +type Key struct { + ID string `json:"key-name,omitzero"` + CryptographicAlgorithm string `json:"cryptographic-algorithm,omitzero"` + PreSharedKey PreSharedKey `json:"key-string,omitzero"` + StartLifetime Lifetime `json:"send-lifetime,omitzero"` + AcceptLifetime Lifetime `json:"accept-lifetime,omitzero"` +} + +type PreSharedKey struct { + Secret string `json:"password,omitzero"` +} + +type Lifetime struct { + Duration uint32 `json:"duration,omitzero"` + StartTime CiscoTime `json:"start-time,omitzero"` +} + +type CiscoTime struct { + DayOfMonth int `json:"day-of-month,omitzero"` + Hour int `json:"hour,omitzero"` + Minute int `json:"minute,omitzero"` + Month string `json:"month,omitzero"` + Second int `json:"second,omitzero"` + Year int `json:"year,omitzero"` +} + +func (k *KeyChain) XPath() string { + return "Cisco-IOS-XR-um-key-chain-cfg:key/chains/chain[key-chain-name=" + k.Name + "]" +} + +func NewLifetime(endTime string) (Lifetime, error) { + start := time.Now() + + end, err := time.Parse(time.RFC3339, endTime) + if err != nil { + return Lifetime{}, err + } + duration := end.Sub(start) + + t := new(CiscoTime) + t.DayOfMonth = end.Day() + t.Hour = end.Hour() + t.Minute = end.Minute() + t.Month = strings.ToLower(end.Month().String()) + t.Second = end.Second() + t.Year = end.Year() + + return Lifetime{ + Duration: uint32(duration.Seconds()), + StartTime: *t, + }, nil +} + +func (k *KeyChainOperData) XPath() string { + return "Cisco-IOS-XR-lib-keychain-oper:keychain/keys/key[key-name=" + k.Name + "]" +} + +// KeyChain operational data structures for reading state information +type KeyChainOperData struct { + Name string `json:"-"` + Key KeyOper `json:"key"` +} + +type KeyOper struct { + Keys []KeyStatus `json:"key-id"` +} + +type KeyStatus struct { + ID string `json:"key-id"` + AcceptLifetime StatusLifetime `json:"accept-lifetime"` + CryptographicAlgorithm string `json:"cryptographic-algorithm"` + SendLifetime StatusLifetime `json:"send-lifetime"` + Type string `json:"type"` +} + +type StatusLifetime struct { + Duration string `json:"duration"` + IsAlwaysValid bool `json:"is-always-valid"` + IsValidNow bool `json:"is-valid-now"` + Start string `json:"start"` +} + +type KeyIDOperational struct { + AcceptLifetime LifetimeOperational `json:"accept-lifetime"` + CryptographicAlgorithm string `json:"cryptographic-algorithm"` + KeyID string `json:"key-id"` + SendLifetime LifetimeOperational `json:"send-lifetime"` + Type string `json:"type"` +} + +type LifetimeOperational struct { + Duration string `json:"duration"` + IsAlwaysValid bool `json:"is-always-valid"` + IsValidNow bool `json:"is-valid-now"` + Start string `json:"start"` +} diff --git a/internal/provider/cisco/iosxr/provider.go b/internal/provider/cisco/iosxr/provider.go index 23258ca67..e36578af1 100644 --- a/internal/provider/cisco/iosxr/provider.go +++ b/internal/provider/cisco/iosxr/provider.go @@ -24,6 +24,7 @@ var ( _ provider.DeviceProvider = &Provider{} _ provider.InterfaceProvider = &Provider{} _ provider.VRFProvider = &Provider{} + _ provider.MacSecProvider = &Provider{} ) type Provider struct { @@ -410,6 +411,66 @@ func (p *Provider) DeleteVRF(ctx context.Context, req *provider.VRFRequest) erro return p.client.Delete(ctx, vrf) } +func (p *Provider) EnsureMacSec(ctx context.Context, req *provider.EnsureMacSecRequest) error { + //Configure MacSec Policy + + cipherSuite, err := ExtractCipherSuite(req.MacSec.Spec.Policy.CipherSuite) + if err != nil { + return err + } + + policy := &MacSecPolicy{ + Name: req.MacSec.Spec.Name, + CipherSuite: cipherSuite, + ConfOffset: fmt.Sprintf(ConfOffsetPrefix, req.MacSec.Spec.Policy.ConfidentialityOffset), + KeyServerPriority: req.MacSec.Spec.Policy.KeyServerPriority, + RelayProtection: req.MacSec.Spec.Policy.RelayProtection, + } + + //Configure KeyChain + + chain := new(KeyChain) + chain.Name = req.MacSec.Spec.Name + + for _, psk := range req.Secrets { + lifeTime, err := NewLifetime(string(psk.Data["lifetime"])) + if err != nil { + return err + } + + key := Key{ + ID: string(psk.Data["connectivityKeyName"]), + CryptographicAlgorithm: string(psk.Data["algorithm"]), + StartLifetime: lifeTime, + AcceptLifetime: lifeTime, + } + chain.Keys.Key = append(chain.Keys.Key, key) + } + + return p.client.Update(ctx, policy, chain) +} + +func (p *Provider) DeleteMacSec(ctx context.Context, req *provider.DeleteMacSecRequest) error { + policy := new(MacSecPolicy) + policy.Name = req.MacSec.Spec.Name + + keyChain := new(KeyChain) + keyChain.Name = req.MacSec.Spec.Name + + return p.client.Delete(ctx, policy, keyChain) +} + +func (p *Provider) GetMacSecStatus(ctx context.Context, req *provider.EnsureMacSecRequest) (provider.MacSecStatus, error) { + status := new(KeyChainOperData) + status.Name = req.MacSec.Spec.Name + + err := p.client.GetState(ctx, status) + if err != nil { + return provider.MacSecStatus{}, fmt.Errorf("failed to get MacSec status for %s: %w", req.MacSec.Spec.Name, err) + } + return provider.MacSecStatus{}, nil +} + func init() { provider.Register("cisco-iosxr-gnmi", NewProvider) } diff --git a/internal/provider/cisco/iosxr/provider_test.go b/internal/provider/cisco/iosxr/provider_test.go index 49083fb8f..292f03787 100644 --- a/internal/provider/cisco/iosxr/provider_test.go +++ b/internal/provider/cisco/iosxr/provider_test.go @@ -116,6 +116,10 @@ func (m *MockClient) GetConfig(ctx context.Context, configs ...gnmiext.DataEleme return nil } +func (m *MockClient) Create(ctx context.Context, conf ...gnmiext.DataElement) error { + return nil +} + func (m *MockClient) GetState(ctx context.Context, states ...gnmiext.DataElement) error { if m.GetStateFunc != nil { return m.GetStateFunc(ctx, states...) diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index 09302ca23..68167bbff 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -48,6 +48,7 @@ var ( _ provider.EVPNInstanceProvider = (*Provider)(nil) _ provider.InterfaceProvider = (*Provider)(nil) _ provider.ISISProvider = (*Provider)(nil) + _ provider.MacSecProvider = (*Provider)(nil) _ provider.ManagementAccessProvider = (*Provider)(nil) _ provider.NTPProvider = (*Provider)(nil) _ provider.OSPFProvider = (*Provider)(nil) @@ -3270,6 +3271,27 @@ func (p *Provider) GetDHCPRelayStatus(ctx context.Context, req *provider.DHCPRel return s, nil } +// EnsureMacSec is a dummy implementation for MacSec provisioning. +// This method currently returns an error indicating that MacSec is not yet supported. +func (p *Provider) EnsureMacSec(ctx context.Context, req *provider.EnsureMacSecRequest) error { + // TODO(sven-rosenzweig): Implement MacSec + return nil +} + +// DeleteMacSec is a dummy implementation for MacSec deletion. +// This method currently returns an error indicating that MacSec is not yet supported. +func (p *Provider) DeleteMacSec(ctx context.Context, req *provider.DeleteMacSecRequest) error { + // TODO(sven-rosenzweig) : Implement MacSec deletion + return nil +} + +// DeleteMacSec is a dummy implementation for MacSec status retrieval. +// This method currently returns an error indicating that MacSec is not yet supported. +func (p *Provider) GetMacSecStatus(ctx context.Context, req *provider.EnsureMacSecRequest) (provider.MacSecStatus, error) { + // TODO(sven-rosenzweig): Implement MacSec + return provider.MacSecStatus{}, nil +} + func init() { provider.Register("cisco-nxos-gnmi", NewProvider) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a9d7f9883..e7ca21298 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1,5 +1,6 @@ // SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors // SPDX-License-Identifier: Apache-2.0 + package provider import ( @@ -12,6 +13,7 @@ import ( "sync" "time" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -614,6 +616,41 @@ type DeleteRoutingPolicyRequest struct { Name string } +// MacSecProvider is the interface for the realization of the MacSec objects over different providers. +type MacSecProvider interface { + Provider + + // EnsureMacSec call is responsible for MacSec realization on the provider. + EnsureMacSec(context.Context, *EnsureMacSecRequest) error + // DeleteMacSec call is responsible for MacSec deletion on the provider. + DeleteMacSec(context.Context, *DeleteMacSecRequest) error + // GetMacSecStatus call is responsible for retrieving the current status of the MacSec from the provider. + GetMacSecStatus(context.Context, *EnsureMacSecRequest) (MacSecStatus, error) +} + +type EnsureMacSecRequest struct { + MacSec *v1alpha1.MacSec + Secrets []corev1.Secret + // SourceInterface is the interface on which MacSec is configured. + SourceInterface *v1alpha1.Interface +} + +type DeleteMacSecRequest struct { + MacSec *v1alpha1.MacSec +} + +type MacSecStatus struct { + // OverallStatus indicates whether all keys are valid + OverallStatus bool + // KeyStatuses provides the validity status for each individual key configured on the device. + KeyStatus []KeyValidity +} + +type KeyValidity struct { + KeyName string + Valid bool +} + type NVEProvider interface { Provider