From 7c68895c77361ad110fd6ac479203e9c4f442c7b Mon Sep 17 00:00:00 2001 From: Swarup Ghosh Date: Thu, 19 Feb 2026 18:12:26 +0530 Subject: [PATCH 1/3] oape: generate API types and tests from EP #1834 Adds NetworkPolicy API type improvements and comprehensive integration tests for the ExternalSecretsConfig networkPolicies field from EP #1834. Changes: - Fix godoc comments on ComponentName constants (trailing periods) - Add DNS subdomain validation pattern for NetworkPolicy name field - Improve Egress field documentation for clarity - Fix Egress JSON tag (remove omitempty for Required field) - Fix listType marker spacing - Add 15 new integration test cases covering: - NetworkPolicy creation for CoreController and BitwardenSDKServer - Multiple networkPolicies in a single config - Allow-all egress and deny-all egress configurations - DNS name validation (uppercase, underscores, leading/trailing hyphens) - Empty networkPolicies list handling - Name length validation - Invalid componentName validation - NetworkPolicy addition after creation (onUpdate) - Immutability of name and componentName fields (onUpdate) - Regenerated CRD manifests with updated validation Co-Authored-By: Claude Opus 4.6 --- api/v1alpha1/external_secrets_config_types.go | 23 +- .../externalsecretsconfig.testsuite.yaml | 381 +++++++++++++++++- ...r.openshift.io_externalsecretsconfigs.yaml | 15 +- 3 files changed, 400 insertions(+), 19 deletions(-) diff --git a/api/v1alpha1/external_secrets_config_types.go b/api/v1alpha1/external_secrets_config_types.go index f06a7df79..c8b5b3a3c 100644 --- a/api/v1alpha1/external_secrets_config_types.go +++ b/api/v1alpha1/external_secrets_config_types.go @@ -216,10 +216,10 @@ type CertProvidersConfig struct { type ComponentName string const ( - // CoreController represents the external-secrets component + // ExternalSecretsCoreController represents the external-secrets core controller component. CoreController ComponentName = "ExternalSecretsCoreController" - // BitwardenSDKServer represents the bitwarden-sdk-server component + // BitwardenSDKServer represents the bitwarden-sdk-server component. BitwardenSDKServer ComponentName = "BitwardenSDKServer" ) @@ -228,8 +228,11 @@ const ( type NetworkPolicy struct { // name is a unique identifier for this network policy configuration. // This name will be used as part of the generated NetworkPolicy resource name. + // The value must be a valid DNS subdomain name consisting of lowercase alphanumeric characters or '-', + // starting and ending with an alphanumeric character. // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=253 + // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$` // +kubebuilder:validation:Required Name string `json:"name"` @@ -238,14 +241,12 @@ type NetworkPolicy struct { // +kubebuilder:validation:Required ComponentName ComponentName `json:"componentName"` - // egress is a list of egress rules to be applied to the selected pods. Outgoing traffic - // is allowed if there are no NetworkPolicies selecting the pod (and cluster policy - // otherwise allows the traffic), OR if the traffic matches at least one egress rule - // across all the NetworkPolicy objects whose podSelector matches the pod. If - // this field is empty then this NetworkPolicy limits all outgoing traffic (and serves - // solely to ensure that the pods it selects are isolated by default). - // The operator will automatically handle ingress rules based on the current running ports. + // egress is a list of egress rules to be applied to the selected component pods. + // The operator generates a Kubernetes NetworkPolicy targeting the component specified by componentName, + // using the egress rules provided here. If this list is empty, the generated NetworkPolicy will deny + // all outgoing traffic for the component (default-deny egress). + // The operator will automatically handle ingress rules based on the component's required ports. // +kubebuilder:validation:Required - //+listType=atomic - Egress []networkingv1.NetworkPolicyEgressRule `json:"egress,omitempty" protobuf:"bytes,3,rep,name=egress"` + // +listType=atomic + Egress []networkingv1.NetworkPolicyEgressRule `json:"egress"` } diff --git a/api/v1alpha1/tests/externalsecretsconfig.operator.openshift.io/externalsecretsconfig.testsuite.yaml b/api/v1alpha1/tests/externalsecretsconfig.operator.openshift.io/externalsecretsconfig.testsuite.yaml index 720d7fc23..3e5da3ccd 100644 --- a/api/v1alpha1/tests/externalsecretsconfig.operator.openshift.io/externalsecretsconfig.testsuite.yaml +++ b/api/v1alpha1/tests/externalsecretsconfig.operator.openshift.io/externalsecretsconfig.testsuite.yaml @@ -493,6 +493,283 @@ tests: webhookConfig: certificateCheckInterval: "15m" operatingNamespace: "test-ns" + - name: Should be able to create ExternalSecretsConfig with networkPolicies for CoreController + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-external-secrets-egress + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-external-secrets-egress + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + - name: Should be able to create ExternalSecretsConfig with networkPolicies for BitwardenSDKServer + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-bitwarden-egress + componentName: BitwardenSDKServer + egress: + - ports: + - protocol: TCP + port: 6443 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-bitwarden-egress + componentName: BitwardenSDKServer + egress: + - ports: + - protocol: TCP + port: 6443 + - name: Should be able to create ExternalSecretsConfig with multiple networkPolicies + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-core-egress + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + - name: allow-bitwarden-egress + componentName: BitwardenSDKServer + egress: + - ports: + - protocol: TCP + port: 6443 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-core-egress + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + - name: allow-bitwarden-egress + componentName: BitwardenSDKServer + egress: + - ports: + - protocol: TCP + port: 6443 + - name: Should be able to create ExternalSecretsConfig with networkPolicies allowing all egress + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-all-egress + componentName: ExternalSecretsCoreController + egress: + - {} + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-all-egress + componentName: ExternalSecretsCoreController + egress: + - {} + - name: Should fail with invalid componentName in networkPolicies + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-egress + componentName: InvalidComponent + egress: + - ports: + - protocol: TCP + port: 6443 + expectedError: "spec.controllerConfig.networkPolicies[0].componentName: Unsupported value" + - name: Should fail with empty name in networkPolicies + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: "" + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + expectedError: "spec.controllerConfig.networkPolicies[0].name: Invalid value" + - name: Should fail with networkPolicy name too long + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: "this-network-policy-name-is-extremely-long-and-exceeds-the-kubernetes-maximum-name-length-limit-of-two-hundred-fifty-three-characters-which-is-quite-a-lot-of-characters-but-we-need-to-test-this-validation-constraint-properly-to-ensure-it-works-as-expected-in-all-scenarios" + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + expectedError: "spec.controllerConfig.networkPolicies[0].name: Too long" + - name: Should fail with networkPolicy name containing uppercase characters + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: "Allow-Egress" + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + expectedError: "spec.controllerConfig.networkPolicies[0].name: Invalid value" + - name: Should fail with networkPolicy name containing underscores + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: "allow_egress" + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + expectedError: "spec.controllerConfig.networkPolicies[0].name: Invalid value" + - name: Should fail with networkPolicy name starting with hyphen + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: "-allow-egress" + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + expectedError: "spec.controllerConfig.networkPolicies[0].name: Invalid value" + - name: Should fail with networkPolicy name ending with hyphen + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: "allow-egress-" + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + expectedError: "spec.controllerConfig.networkPolicies[0].name: Invalid value" + - name: Should be able to create ExternalSecretsConfig with networkPolicy with empty egress list for deny-all + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: deny-all-egress + componentName: ExternalSecretsCoreController + egress: [] + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: deny-all-egress + componentName: ExternalSecretsCoreController + egress: [] + - name: Should be able to create ExternalSecretsConfig with valid DNS subdomain networkPolicy name + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-core-controller-egress-to-api-server + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-core-controller-egress-to-api-server + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + - name: Should be able to create ExternalSecretsConfig with empty networkPolicies list + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: [] + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: [] onUpdate: - name: Should be able to update labels in controller config resourceName: cluster @@ -596,4 +873,106 @@ tests: bitwardenSecretManagerProvider: mode: Enabled secretRef: - name: "bitwarden-certs" \ No newline at end of file + name: "bitwarden-certs" + - name: Should be able to add networkPolicies after creation + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: {} + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-core-egress + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-core-egress + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + - name: Should not be able to change name and componentName of existing networkPolicies + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-core-egress + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: renamed-policy + componentName: BitwardenSDKServer + egress: + - ports: + - protocol: TCP + port: 6443 + expectedError: "name and componentName fields in networkPolicies are immutable" + - name: Should be able to update egress rules in existing networkPolicy + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-core-egress + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-core-egress + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + - ports: + - protocol: TCP + port: 443 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + networkPolicies: + - name: allow-core-egress + componentName: ExternalSecretsCoreController + egress: + - ports: + - protocol: TCP + port: 6443 + - ports: + - protocol: TCP + port: 443 \ No newline at end of file diff --git a/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml b/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml index ae6890cdc..0f962a004 100644 --- a/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml +++ b/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml @@ -1297,13 +1297,11 @@ spec: type: string egress: description: |- - egress is a list of egress rules to be applied to the selected pods. Outgoing traffic - is allowed if there are no NetworkPolicies selecting the pod (and cluster policy - otherwise allows the traffic), OR if the traffic matches at least one egress rule - across all the NetworkPolicy objects whose podSelector matches the pod. If - this field is empty then this NetworkPolicy limits all outgoing traffic (and serves - solely to ensure that the pods it selects are isolated by default). - The operator will automatically handle ingress rules based on the current running ports. + egress is a list of egress rules to be applied to the selected component pods. + The operator generates a Kubernetes NetworkPolicy targeting the component specified by componentName, + using the egress rules provided here. If this list is empty, the generated NetworkPolicy will deny + all outgoing traffic for the component (default-deny egress). + The operator will automatically handle ingress rules based on the component's required ports. items: description: |- NetworkPolicyEgressRule describes a particular set of traffic that is allowed out of pods @@ -1497,8 +1495,11 @@ spec: description: |- name is a unique identifier for this network policy configuration. This name will be used as part of the generated NetworkPolicy resource name. + The value must be a valid DNS subdomain name consisting of lowercase alphanumeric characters or '-', + starting and ending with an alphanumeric character. maxLength: 253 minLength: 1 + pattern: ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ type: string required: - componentName From 33caffb8f5a5dedb20f9947dd60f2c268d203f99 Mon Sep 17 00:00:00 2001 From: Swarup Ghosh Date: Thu, 19 Feb 2026 18:17:55 +0530 Subject: [PATCH 2/3] oape: implement controller from EP #1834 Adds stale custom NetworkPolicy cleanup logic and RBAC improvements for the NetworkPolicy feature from EP #1834. Changes: - Add RBAC delete verb for networkpolicies to enable cleanup - Add custom network policy label for lifecycle management - Implement deleteStaleCustomNetworkPolicies() to remove NetworkPolicies that are no longer referenced in ExternalSecretsConfig spec - Add comprehensive unit tests for stale policy cleanup (no stale, single stale, all stale, list error, delete error) - Update buildNetworkPolicyFromConfig to apply custom label - Regenerated RBAC role.yaml Co-Authored-By: Claude Opus 4.6 --- config/rbac/role.yaml | 1 + pkg/controller/external_secrets/controller.go | 2 +- .../external_secrets/networkpolicy.go | 69 ++++++- .../external_secrets/networkpolicy_test.go | 193 +++++++++++++++++- 4 files changed, 259 insertions(+), 6 deletions(-) diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index f0b7386b7..c93ef271e 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -163,6 +163,7 @@ rules: - networkpolicies verbs: - create + - delete - get - list - update diff --git a/pkg/controller/external_secrets/controller.go b/pkg/controller/external_secrets/controller.go index e5771c823..47f0cb62d 100644 --- a/pkg/controller/external_secrets/controller.go +++ b/pkg/controller/external_secrets/controller.go @@ -104,7 +104,7 @@ type Reconciler struct { // +kubebuilder:rbac:groups="",resources=events;secrets;services;serviceaccounts,verbs=get;list;watch;create;update;delete;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update // +kubebuilder:rbac:groups=cert-manager.io,resources=certificates;clusterissuers;issuers,verbs=get;list;watch;create;update -// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update +// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;delete // +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch;create // +kubebuilder:rbac:groups="",resources=endpoints,verbs=get;list;watch;create diff --git a/pkg/controller/external_secrets/networkpolicy.go b/pkg/controller/external_secrets/networkpolicy.go index 2e735c2fe..2a320534b 100644 --- a/pkg/controller/external_secrets/networkpolicy.go +++ b/pkg/controller/external_secrets/networkpolicy.go @@ -6,6 +6,7 @@ import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" operatorv1alpha1 "github.com/openshift/external-secrets-operator/api/v1alpha1" @@ -13,6 +14,15 @@ import ( "github.com/openshift/external-secrets-operator/pkg/operator/assets" ) +const ( + // customNetworkPolicyLabelKey is the label key applied to custom network policies + // created from the ExternalSecretsConfig API to distinguish them from static policies. + customNetworkPolicyLabelKey = "operator.openshift.io/custom-network-policy" + + // customNetworkPolicyLabelValue is the label value for custom network policies. + customNetworkPolicyLabelValue = "true" +) + // createOrApplyNetworkPolicies handles creation of both static network policies from manifests // and custom network policies configured in the ExternalSecretsConfig API. func (r *Reconciler) createOrApplyNetworkPolicies(esc *operatorv1alpha1.ExternalSecretsConfig, resourceLabels map[string]string, externalSecretsConfigCreateRecon bool) error { @@ -75,9 +85,21 @@ func (r *Reconciler) createOrApplyStaticNetworkPolicies(esc *operatorv1alpha1.Ex return nil } -// createOrApplyCustomNetworkPolicies applies custom network policies defined in the ExternalSecretsConfig spec. +// createOrApplyCustomNetworkPolicies applies custom network policies defined in the ExternalSecretsConfig spec +// and removes any stale custom network policies that are no longer in the spec. func (r *Reconciler) createOrApplyCustomNetworkPolicies(esc *operatorv1alpha1.ExternalSecretsConfig, resourceLabels map[string]string, externalSecretsConfigCreateRecon bool) error { - if esc.Spec.ControllerConfig.NetworkPolicies == nil { + // Build a set of desired custom network policy names for stale cleanup + desiredNames := make(map[string]struct{}) + for _, npConfig := range esc.Spec.ControllerConfig.NetworkPolicies { + desiredNames[npConfig.Name] = struct{}{} + } + + // Clean up stale custom network policies that are no longer in the spec + if err := r.deleteStaleCustomNetworkPolicies(esc, desiredNames); err != nil { + return err + } + + if len(esc.Spec.ControllerConfig.NetworkPolicies) == 0 { r.log.V(4).Info("No custom network policies configured in ControllerConfig") return nil } @@ -91,6 +113,39 @@ func (r *Reconciler) createOrApplyCustomNetworkPolicies(esc *operatorv1alpha1.Ex return nil } +// deleteStaleCustomNetworkPolicies lists all custom network policies in the namespace and deletes +// those that are no longer present in the ExternalSecretsConfig spec. +func (r *Reconciler) deleteStaleCustomNetworkPolicies(esc *operatorv1alpha1.ExternalSecretsConfig, desiredNames map[string]struct{}) error { + namespace := getNamespace(esc) + + // List all custom network policies (identified by the custom label) + existingList := &networkingv1.NetworkPolicyList{} + labelSelector := labels.SelectorFromSet(labels.Set{ + customNetworkPolicyLabelKey: customNetworkPolicyLabelValue, + requestEnqueueLabelKey: requestEnqueueLabelValue, + }) + if err := r.List(r.ctx, existingList, &client.ListOptions{ + Namespace: namespace, + LabelSelector: labelSelector, + }); err != nil { + return common.FromClientError(err, "failed to list custom network policies in namespace %s", namespace) + } + + for i := range existingList.Items { + np := &existingList.Items[i] + if _, desired := desiredNames[np.Name]; !desired { + networkPolicyName := fmt.Sprintf("%s/%s", np.Namespace, np.Name) + r.log.V(1).Info("Deleting stale custom network policy", "name", networkPolicyName) + if err := r.Delete(r.ctx, np); err != nil { + return common.FromClientError(err, "failed to delete stale network policy %s", networkPolicyName) + } + r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "Stale NetworkPolicy %s deleted", networkPolicyName) + } + } + + return nil +} + // createOrApplyCustomNetworkPolicy creates or updates a custom network policy based on API configuration. func (r *Reconciler) createOrApplyCustomNetworkPolicy(esc *operatorv1alpha1.ExternalSecretsConfig, npConfig operatorv1alpha1.NetworkPolicy, resourceLabels map[string]string, externalSecretsConfigCreateRecon bool) error { // Build the NetworkPolicy object from the API spec @@ -179,12 +234,18 @@ func (r *Reconciler) buildNetworkPolicyFromConfig(esc *operatorv1alpha1.External return nil, fmt.Errorf("failed to determine pod selector for network policy %s: %w", npConfig.Name, err) } - // Build the NetworkPolicy object + // Build the NetworkPolicy object with custom label for lifecycle management + npLabels := make(map[string]string, len(resourceLabels)+1) + for k, v := range resourceLabels { + npLabels[k] = v + } + npLabels[customNetworkPolicyLabelKey] = customNetworkPolicyLabelValue + networkPolicy := &networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: npConfig.Name, Namespace: namespace, - Labels: resourceLabels, + Labels: npLabels, }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: podSelector, diff --git a/pkg/controller/external_secrets/networkpolicy_test.go b/pkg/controller/external_secrets/networkpolicy_test.go index 55eab12ef..c1b72eaf3 100644 --- a/pkg/controller/external_secrets/networkpolicy_test.go +++ b/pkg/controller/external_secrets/networkpolicy_test.go @@ -421,6 +421,196 @@ func TestGetPodSelectorForComponent(t *testing.T) { } } +func TestDeleteStaleCustomNetworkPolicies(t *testing.T) { + tests := []struct { + name string + preReq func(*Reconciler, *fakes.FakeCtrlClient) + updateExternalSecretsConfig func(*operatorv1alpha1.ExternalSecretsConfig) + wantErr string + wantDeleteCount int + }{ + { + name: "no stale policies to delete", + preReq: func(r *Reconciler, m *fakes.FakeCtrlClient) { + m.ListCalls(func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + npList := list.(*networkingv1.NetworkPolicyList) + npList.Items = []networkingv1.NetworkPolicy{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "keep-policy", + Namespace: externalsecretsDefaultNamespace, + Labels: map[string]string{ + customNetworkPolicyLabelKey: customNetworkPolicyLabelValue, + requestEnqueueLabelKey: requestEnqueueLabelValue, + }, + }, + }, + } + return nil + }) + }, + updateExternalSecretsConfig: func(esc *operatorv1alpha1.ExternalSecretsConfig) { + esc.Spec.ControllerConfig.NetworkPolicies = []operatorv1alpha1.NetworkPolicy{ + { + Name: "keep-policy", + ComponentName: operatorv1alpha1.CoreController, + Egress: []networkingv1.NetworkPolicyEgressRule{}, + }, + } + }, + wantDeleteCount: 0, + }, + { + name: "stale policy deleted successfully", + preReq: func(r *Reconciler, m *fakes.FakeCtrlClient) { + m.ListCalls(func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + npList := list.(*networkingv1.NetworkPolicyList) + npList.Items = []networkingv1.NetworkPolicy{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "keep-policy", + Namespace: externalsecretsDefaultNamespace, + Labels: map[string]string{ + customNetworkPolicyLabelKey: customNetworkPolicyLabelValue, + requestEnqueueLabelKey: requestEnqueueLabelValue, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "stale-policy", + Namespace: externalsecretsDefaultNamespace, + Labels: map[string]string{ + customNetworkPolicyLabelKey: customNetworkPolicyLabelValue, + requestEnqueueLabelKey: requestEnqueueLabelValue, + }, + }, + }, + } + return nil + }) + m.DeleteCalls(func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + return nil + }) + }, + updateExternalSecretsConfig: func(esc *operatorv1alpha1.ExternalSecretsConfig) { + esc.Spec.ControllerConfig.NetworkPolicies = []operatorv1alpha1.NetworkPolicy{ + { + Name: "keep-policy", + ComponentName: operatorv1alpha1.CoreController, + Egress: []networkingv1.NetworkPolicyEgressRule{}, + }, + } + }, + wantDeleteCount: 1, + }, + { + name: "all custom policies deleted when spec is empty", + preReq: func(r *Reconciler, m *fakes.FakeCtrlClient) { + m.ListCalls(func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + npList := list.(*networkingv1.NetworkPolicyList) + npList.Items = []networkingv1.NetworkPolicy{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "old-policy-1", + Namespace: externalsecretsDefaultNamespace, + Labels: map[string]string{ + customNetworkPolicyLabelKey: customNetworkPolicyLabelValue, + requestEnqueueLabelKey: requestEnqueueLabelValue, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "old-policy-2", + Namespace: externalsecretsDefaultNamespace, + Labels: map[string]string{ + customNetworkPolicyLabelKey: customNetworkPolicyLabelValue, + requestEnqueueLabelKey: requestEnqueueLabelValue, + }, + }, + }, + } + return nil + }) + m.DeleteCalls(func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + return nil + }) + }, + wantDeleteCount: 2, + }, + { + name: "list fails", + preReq: func(r *Reconciler, m *fakes.FakeCtrlClient) { + m.ListCalls(func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + return commontest.TestClientError + }) + }, + wantErr: "failed to list custom network policies in namespace external-secrets: test client error", + }, + { + name: "delete fails", + preReq: func(r *Reconciler, m *fakes.FakeCtrlClient) { + m.ListCalls(func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + npList := list.(*networkingv1.NetworkPolicyList) + npList.Items = []networkingv1.NetworkPolicy{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "stale-policy", + Namespace: externalsecretsDefaultNamespace, + Labels: map[string]string{ + customNetworkPolicyLabelKey: customNetworkPolicyLabelValue, + requestEnqueueLabelKey: requestEnqueueLabelValue, + }, + }, + }, + } + return nil + }) + m.DeleteCalls(func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + return commontest.TestClientError + }) + }, + wantErr: "failed to delete stale network policy external-secrets/stale-policy: test client error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := testReconciler(t) + mock := &fakes.FakeCtrlClient{} + r.CtrlClient = mock + if tt.preReq != nil { + tt.preReq(r, mock) + } + + esc := commontest.TestExternalSecretsConfig() + if tt.updateExternalSecretsConfig != nil { + tt.updateExternalSecretsConfig(esc) + } + + // Build desired names + desiredNames := make(map[string]struct{}) + for _, npConfig := range esc.Spec.ControllerConfig.NetworkPolicies { + desiredNames[npConfig.Name] = struct{}{} + } + + err := r.deleteStaleCustomNetworkPolicies(esc, desiredNames) + if tt.wantErr != "" { + if err == nil || err.Error() != tt.wantErr { + t.Errorf("Expected error: %v, got: %v", tt.wantErr, err) + } + } else if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if tt.wantDeleteCount > 0 && mock.DeleteCallCount() != tt.wantDeleteCount { + t.Errorf("Expected %d delete calls, got %d", tt.wantDeleteCount, mock.DeleteCallCount()) + } + }) + } +} + func TestBuildNetworkPolicyFromConfig(t *testing.T) { tests := []struct { name string @@ -450,7 +640,8 @@ func TestBuildNetworkPolicyFromConfig(t *testing.T) { np.Spec.PodSelector.MatchLabels["app.kubernetes.io/name"] == "external-secrets" && len(np.Spec.Egress) == 1 && len(np.Spec.PolicyTypes) == 1 && - np.Spec.PolicyTypes[0] == networkingv1.PolicyTypeEgress + np.Spec.PolicyTypes[0] == networkingv1.PolicyTypeEgress && + np.Labels[customNetworkPolicyLabelKey] == customNetworkPolicyLabelValue }, }, { From a74824a11a7a19a1d983d10049ad586189d50c09 Mon Sep 17 00:00:00 2001 From: Swarup Ghosh Date: Thu, 19 Feb 2026 18:25:31 +0530 Subject: [PATCH 3/3] oape: fix DNS network policy coverage for webhook and cert-controller pods The DNS allow policy was only covering external-secrets and bitwarden-sdk-server pods, leaving webhook and cert-controller pods without DNS resolution capability. This would prevent those pods from resolving the API server hostname, effectively breaking their API server connectivity. Add external-secrets-webhook and external-secrets-cert-controller to the DNS policy podSelector matchExpressions to ensure all operand components can resolve DNS names. Co-Authored-By: Claude Opus 4.6 --- bindata/external-secrets/networkpolicy_allow-dns.yaml | 2 ++ pkg/operator/assets/bindata.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bindata/external-secrets/networkpolicy_allow-dns.yaml b/bindata/external-secrets/networkpolicy_allow-dns.yaml index 5e39bb775..a89e0e9ba 100644 --- a/bindata/external-secrets/networkpolicy_allow-dns.yaml +++ b/bindata/external-secrets/networkpolicy_allow-dns.yaml @@ -14,6 +14,8 @@ spec: operator: In values: - external-secrets + - external-secrets-webhook + - external-secrets-cert-controller - bitwarden-sdk-server egress: - to: diff --git a/pkg/operator/assets/bindata.go b/pkg/operator/assets/bindata.go index 4b9004789..e4ed42910 100644 --- a/pkg/operator/assets/bindata.go +++ b/pkg/operator/assets/bindata.go @@ -354,6 +354,8 @@ spec: operator: In values: - external-secrets + - external-secrets-webhook + - external-secrets-cert-controller - bitwarden-sdk-server egress: - to: