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 diff --git a/output/e2e_external-secrets-operator/e2e-suggestions.md b/output/e2e_external-secrets-operator/e2e-suggestions.md new file mode 100644 index 000000000..4a2ff9bb3 --- /dev/null +++ b/output/e2e_external-secrets-operator/e2e-suggestions.md @@ -0,0 +1,67 @@ +# E2E Test Suggestions: external-secrets-operator + +## Detected Operator Structure +- **Framework**: controller-runtime (kubebuilder v4 / operator-sdk) +- **Managed CRDs**: ExternalSecretsConfig (cluster-scoped singleton), ExternalSecretsManager +- **E2E Pattern**: Ginkgo v2 with dynamic client, build tag `e2e` +- **Operator Namespace**: `external-secrets-operator` +- **Operand Namespace**: `external-secrets` + +## Changes Detected in Diff + +| File | Change | Impact | +|------|--------|--------| +| `api/v1alpha1/external_secrets_config_types.go` | Added DNS pattern validation on `name` field; fixed `egress` JSON tag | API validation changes | +| `config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml` | CRD schema updated with `pattern` field | Admission validation | +| `api/v1alpha1/tests/.../externalsecretsconfig.testsuite.yaml` | Added test cases for DNS pattern, empty egress, egress updates | Integration test coverage | + +## Highly Recommended E2E Scenarios + +### 1. Custom NetworkPolicy Lifecycle (Critical) +**Reason**: The NetworkPolicy feature is the core of EP #1834. End-to-end validation that the operator correctly creates, updates, and deletes Kubernetes NetworkPolicy resources based on the API spec is essential. + +**Tests**: +- Create ExternalSecretsConfig with custom networkPolicies -> verify NetworkPolicy resources exist +- Update egress rules -> verify NetworkPolicy updated +- Remove policies from spec -> verify stale policies deleted + +### 2. DNS Name Pattern Validation (High) +**Reason**: New pattern validation was added (`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`). E2E tests verify the CRD admission webhook correctly rejects invalid names on a real cluster. + +**Tests**: +- Uppercase names rejected +- Underscore names rejected +- Leading/trailing hyphen names rejected +- Valid names accepted + +### 3. Static NetworkPolicy Verification (High) +**Reason**: The operator creates baseline deny-all and allow policies for security. Verifying these exist ensures the security posture is correct. + +**Tests**: +- deny-all-traffic policy exists and matches all pods +- allow-to-dns policy exists +- Component-specific allow policies exist + +### 4. Empty Egress Deny-All Behavior (Medium) +**Reason**: The `egress` field was changed to remove `omitempty`, enabling explicit empty-list semantics for deny-all egress per component. + +**Tests**: +- Create policy with `egress: []` -> verify NetworkPolicy has Egress policyType but no egress rules + +## Optional/Nice-to-Have Scenarios + +### 5. Component Pod Selector Mapping (Low) +**Reason**: Verify that `ExternalSecretsCoreController` maps to `app.kubernetes.io/name: external-secrets` and `BitwardenSDKServer` maps to `bitwarden-sdk-server`. + +### 6. Multiple NetworkPolicies (Low) +**Reason**: Verify the operator handles multiple custom policies simultaneously. + +### 7. Connectivity Verification (Low, requires workload) +**Reason**: Actually test that network policies block/allow traffic as expected. This requires deploying test pods and attempting connections, which is complex to automate. + +## Gaps / Hard to Test Automatically + +1. **Bitwarden SDK Server scenarios**: Require Bitwarden plugin to be enabled, which needs additional infrastructure +2. **Cert-manager conditional policies**: Require cert-manager to be installed/uninstalled, which is disruptive +3. **Actual network connectivity testing**: Requires deploying test pods to verify that traffic is blocked/allowed per policy rules +4. **DNS resolution verification**: Hard to test that the DNS policy actually allows name resolution without deploying workloads diff --git a/output/e2e_external-secrets-operator/e2e_test.go b/output/e2e_external-secrets-operator/e2e_test.go new file mode 100644 index 000000000..2c1c13117 --- /dev/null +++ b/output/e2e_external-secrets-operator/e2e_test.go @@ -0,0 +1,360 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "context" + "fmt" + "time" + + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var ( + escGVR = schema.GroupVersionResource{ + Group: "operator.openshift.io", + Version: "v1alpha1", + Resource: "externalsecretsconfigs", + } + networkPolicyGVR = schema.GroupVersionResource{ + Group: "networking.k8s.io", + Version: "v1", + Resource: "networkpolicies", + } +) + +// Diff-suggested: NetworkPolicy API changes from EP #1834 +var _ = Describe("NetworkPolicy E2E Tests for ExternalSecretsConfig", Ordered, func() { + const ( + operandNamespace = "external-secrets" + escName = "cluster" + timeout = 60 * time.Second + interval = 2 * time.Second + ) + + // getESC retrieves the ExternalSecretsConfig cluster singleton + getESC := func(ctx context.Context) (*unstructured.Unstructured, error) { + return dynamicClient.Resource(escGVR).Get(ctx, escName, metav1.GetOptions{}) + } + + // getNetworkPolicy retrieves a NetworkPolicy from the operand namespace + getNetworkPolicy := func(ctx context.Context, name string) (*unstructured.Unstructured, error) { + return dynamicClient.Resource(networkPolicyGVR).Namespace(operandNamespace).Get(ctx, name, metav1.GetOptions{}) + } + + // patchESCNetworkPolicies patches the ExternalSecretsConfig with network policies + patchESCNetworkPolicies := func(ctx context.Context, networkPolicies []interface{}) error { + esc, err := getESC(ctx) + if err != nil { + return err + } + + spec, _, _ := unstructured.NestedMap(esc.Object, "spec") + if spec == nil { + spec = map[string]interface{}{} + } + + controllerConfig, _, _ := unstructured.NestedMap(spec, "controllerConfig") + if controllerConfig == nil { + controllerConfig = map[string]interface{}{} + } + + controllerConfig["networkPolicies"] = networkPolicies + spec["controllerConfig"] = controllerConfig + esc.Object["spec"] = spec + + _, err = dynamicClient.Resource(escGVR).Update(ctx, esc, metav1.UpdateOptions{}) + return err + } + + // waitForReady waits for the ESC to reach Ready condition + waitForReady := func(ctx context.Context) { + Eventually(func() bool { + esc, err := getESC(ctx) + if err != nil { + return false + } + conditions, _, _ := unstructured.NestedSlice(esc.Object, "status", "conditions") + for _, c := range conditions { + cond, ok := c.(map[string]interface{}) + if !ok { + continue + } + if cond["type"] == "Ready" && cond["status"] == "True" { + return true + } + } + return false + }, timeout, interval).Should(BeTrue(), "ExternalSecretsConfig should become Ready") + } + + BeforeAll(func() { + By("Verifying ExternalSecretsConfig exists and is Ready") + ctx := context.Background() + waitForReady(ctx) + }) + + AfterAll(func() { + By("Cleaning up custom network policies") + ctx := context.Background() + _ = patchESCNetworkPolicies(ctx, []interface{}{}) + time.Sleep(5 * time.Second) + }) + + // Diff-suggested: Static NetworkPolicy verification (baseline) + Context("Static NetworkPolicies", func() { + It("should have deny-all network policy in operand namespace", func() { + By("Checking for deny-all-traffic NetworkPolicy") + ctx := context.Background() + np, err := getNetworkPolicy(ctx, "deny-all-traffic") + Expect(err).NotTo(HaveOccurred(), "deny-all-traffic NetworkPolicy should exist") + + By("Verifying deny-all selects all pods") + podSelector, _, _ := unstructured.NestedMap(np.Object, "spec", "podSelector") + Expect(podSelector).To(BeEmpty(), "deny-all should have empty podSelector to match all pods") + }) + + It("should have DNS allow policy in operand namespace", func() { + By("Checking for allow-to-dns NetworkPolicy") + ctx := context.Background() + np, err := getNetworkPolicy(ctx, "allow-to-dns") + Expect(err).NotTo(HaveOccurred(), "allow-to-dns NetworkPolicy should exist") + + By("Verifying DNS policy has egress rules") + egress, _, _ := unstructured.NestedSlice(np.Object, "spec", "egress") + Expect(egress).NotTo(BeEmpty(), "DNS policy should have egress rules") + }) + + It("should have main controller allow policy", func() { + By("Checking for main controller NetworkPolicy") + ctx := context.Background() + _, err := getNetworkPolicy(ctx, "allow-api-server-egress-for-main-controller") + Expect(err).NotTo(HaveOccurred(), "main controller NetworkPolicy should exist") + }) + + It("should have webhook allow policy", func() { + By("Checking for webhook NetworkPolicy") + ctx := context.Background() + _, err := getNetworkPolicy(ctx, "allow-api-server-and-webhook-traffic") + Expect(err).NotTo(HaveOccurred(), "webhook NetworkPolicy should exist") + }) + }) + + // Diff-suggested: Custom NetworkPolicy lifecycle (new API field from EP #1834) + Context("Custom NetworkPolicies", func() { + It("should create a custom NetworkPolicy for ExternalSecretsCoreController", func() { + ctx := context.Background() + + By("Patching ESC with a custom network policy") + networkPolicies := []interface{}{ + map[string]interface{}{ + "name": "allow-core-egress", + "componentName": "ExternalSecretsCoreController", + "egress": []interface{}{ + map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "protocol": "TCP", + "port": int64(6443), + }, + }, + }, + }, + }, + } + err := patchESCNetworkPolicies(ctx, networkPolicies) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for reconciliation") + waitForReady(ctx) + + By("Verifying custom NetworkPolicy was created") + Eventually(func() error { + _, err := getNetworkPolicy(ctx, "allow-core-egress") + return err + }, timeout, interval).Should(Succeed(), "Custom NetworkPolicy should be created") + + By("Verifying podSelector targets external-secrets pods") + np, err := getNetworkPolicy(ctx, "allow-core-egress") + Expect(err).NotTo(HaveOccurred()) + matchLabels, _, _ := unstructured.NestedStringMap(np.Object, "spec", "podSelector", "matchLabels") + Expect(matchLabels).To(HaveKeyWithValue("app.kubernetes.io/name", "external-secrets")) + + By("Verifying egress rules") + egress, _, _ := unstructured.NestedSlice(np.Object, "spec", "egress") + Expect(egress).To(HaveLen(1)) + }) + + It("should create a custom NetworkPolicy with empty egress for deny-all", func() { + ctx := context.Background() + + By("Patching ESC with empty egress for deny-all") + networkPolicies := []interface{}{ + map[string]interface{}{ + "name": "deny-core-egress", + "componentName": "ExternalSecretsCoreController", + "egress": []interface{}{}, + }, + } + err := patchESCNetworkPolicies(ctx, networkPolicies) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for reconciliation") + waitForReady(ctx) + + By("Verifying deny-all custom NetworkPolicy was created") + Eventually(func() error { + _, err := getNetworkPolicy(ctx, "deny-core-egress") + return err + }, timeout, interval).Should(Succeed()) + + By("Verifying empty egress rules") + np, err := getNetworkPolicy(ctx, "deny-core-egress") + Expect(err).NotTo(HaveOccurred()) + egress, found, _ := unstructured.NestedSlice(np.Object, "spec", "egress") + if found { + Expect(egress).To(BeEmpty(), "Egress should be empty for deny-all") + } + + By("Verifying policyTypes includes Egress") + policyTypes, _, _ := unstructured.NestedStringSlice(np.Object, "spec", "policyTypes") + Expect(policyTypes).To(ContainElement(string(networkingv1.PolicyTypeEgress))) + }) + + It("should clean up stale custom NetworkPolicies when removed from spec", func() { + ctx := context.Background() + + By("Creating a custom network policy first") + networkPolicies := []interface{}{ + map[string]interface{}{ + "name": "temp-policy", + "componentName": "ExternalSecretsCoreController", + "egress": []interface{}{ + map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{"protocol": "TCP", "port": int64(6443)}, + }, + }, + }, + }, + } + err := patchESCNetworkPolicies(ctx, networkPolicies) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for NetworkPolicy to be created") + Eventually(func() error { + _, err := getNetworkPolicy(ctx, "temp-policy") + return err + }, timeout, interval).Should(Succeed()) + + By("Removing the network policy from spec") + err = patchESCNetworkPolicies(ctx, []interface{}{}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying the stale NetworkPolicy is deleted") + Eventually(func() bool { + _, err := getNetworkPolicy(ctx, "temp-policy") + return errors.IsNotFound(err) + }, timeout, interval).Should(BeTrue(), "Stale NetworkPolicy should be deleted") + }) + }) + + // Diff-suggested: DNS name pattern validation (new pattern validation from EP #1834) + Context("NetworkPolicy Name Validation", func() { + It("should reject names with uppercase characters", func() { + ctx := context.Background() + + By("Attempting to create policy with uppercase name") + networkPolicies := []interface{}{ + map[string]interface{}{ + "name": "Allow-Egress", + "componentName": "ExternalSecretsCoreController", + "egress": []interface{}{}, + }, + } + err := patchESCNetworkPolicies(ctx, networkPolicies) + Expect(err).To(HaveOccurred(), "Uppercase name should be rejected") + Expect(err.Error()).To(ContainSubstring("Invalid value")) + }) + + It("should reject names with underscores", func() { + ctx := context.Background() + + By("Attempting to create policy with underscore name") + networkPolicies := []interface{}{ + map[string]interface{}{ + "name": "allow_egress", + "componentName": "ExternalSecretsCoreController", + "egress": []interface{}{}, + }, + } + err := patchESCNetworkPolicies(ctx, networkPolicies) + Expect(err).To(HaveOccurred(), "Underscore name should be rejected") + }) + + It("should reject names starting with hyphen", func() { + ctx := context.Background() + + By("Attempting to create policy with leading hyphen") + networkPolicies := []interface{}{ + map[string]interface{}{ + "name": "-allow-egress", + "componentName": "ExternalSecretsCoreController", + "egress": []interface{}{}, + }, + } + err := patchESCNetworkPolicies(ctx, networkPolicies) + Expect(err).To(HaveOccurred(), "Leading hyphen name should be rejected") + }) + + It("should reject invalid componentName", func() { + ctx := context.Background() + + By("Attempting to create policy with invalid componentName") + networkPolicies := []interface{}{ + map[string]interface{}{ + "name": "test-policy", + "componentName": "InvalidComponent", + "egress": []interface{}{}, + }, + } + err := patchESCNetworkPolicies(ctx, networkPolicies) + Expect(err).To(HaveOccurred(), "Invalid componentName should be rejected") + Expect(err.Error()).To(ContainSubstring("Unsupported value")) + }) + + It("should accept valid DNS subdomain names", func() { + ctx := context.Background() + + By("Creating policy with valid DNS subdomain name") + networkPolicies := []interface{}{ + map[string]interface{}{ + "name": "allow-core-controller-egress-to-api", + "componentName": "ExternalSecretsCoreController", + "egress": []interface{}{ + map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{"protocol": "TCP", "port": int64(6443)}, + }, + }, + }, + }, + } + err := patchESCNetworkPolicies(ctx, networkPolicies) + Expect(err).NotTo(HaveOccurred(), "Valid DNS subdomain name should be accepted") + + waitForReady(ctx) + + By("Cleaning up") + _ = patchESCNetworkPolicies(ctx, []interface{}{}) + }) + }) +}) diff --git a/output/e2e_external-secrets-operator/execution-steps.md b/output/e2e_external-secrets-operator/execution-steps.md new file mode 100644 index 000000000..b52b01002 --- /dev/null +++ b/output/e2e_external-secrets-operator/execution-steps.md @@ -0,0 +1,220 @@ +# E2E Execution Steps: external-secrets-operator + +## Prerequisites + +```bash +which oc +oc version +oc whoami +oc get nodes +oc get clusterversion +oc get packagemanifests | grep external-secrets +``` + +## Environment Variables + +```bash +export OPERATOR_NAMESPACE="external-secrets-operator" +export OPERAND_NAMESPACE="external-secrets" +export ESC_NAME="cluster" +``` + +## Step 1: Verify Operator Installation + +```bash +# Verify operator is running +oc get pods -n ${OPERATOR_NAMESPACE} -l app.kubernetes.io/name=external-secrets-operator +oc wait --for=condition=Available deployment/external-secrets-operator-controller-manager \ + -n ${OPERATOR_NAMESPACE} --timeout=120s + +# Verify ExternalSecretsConfig exists +oc get externalsecretsconfig ${ESC_NAME} -o yaml +oc wait --for=condition=Ready externalsecretsconfig/${ESC_NAME} --timeout=120s +``` + +## Step 2: Verify Static NetworkPolicies + +```bash +# List all network policies in operand namespace +oc get networkpolicies -n ${OPERAND_NAMESPACE} + +# Verify deny-all policy exists +oc get networkpolicy deny-all-traffic -n ${OPERAND_NAMESPACE} -o yaml + +# Verify DNS allow policy exists and covers all components +oc get networkpolicy allow-to-dns -n ${OPERAND_NAMESPACE} -o yaml + +# Verify main controller allow policy +oc get networkpolicy allow-api-server-egress-for-main-controller -n ${OPERAND_NAMESPACE} -o yaml + +# Verify webhook allow policy +oc get networkpolicy allow-api-server-and-webhook-traffic -n ${OPERAND_NAMESPACE} -o yaml +``` + +## Step 3: Test Custom NetworkPolicy Creation + +```bash +# Patch ExternalSecretsConfig to add a custom network policy +oc patch externalsecretsconfig ${ESC_NAME} --type=merge -p '{ + "spec": { + "controllerConfig": { + "networkPolicies": [ + { + "name": "allow-core-egress", + "componentName": "ExternalSecretsCoreController", + "egress": [ + { + "ports": [ + { + "protocol": "TCP", + "port": 6443 + } + ] + } + ] + } + ] + } + } +}' + +# Wait for reconciliation +sleep 10 +oc wait --for=condition=Ready externalsecretsconfig/${ESC_NAME} --timeout=60s + +# Verify custom NetworkPolicy was created +oc get networkpolicy allow-core-egress -n ${OPERAND_NAMESPACE} -o yaml +``` + +## Step 4: Test DNS Subdomain Name Validation + +```bash +# Test invalid name with uppercase (should fail) +oc patch externalsecretsconfig ${ESC_NAME} --type=merge -p '{ + "spec": { + "controllerConfig": { + "networkPolicies": [ + { + "name": "Allow-Egress", + "componentName": "ExternalSecretsCoreController", + "egress": [{"ports": [{"protocol": "TCP", "port": 6443}]}] + } + ] + } + } +}' 2>&1 | grep -i "invalid" + +# Test invalid name with underscore (should fail) +oc patch externalsecretsconfig ${ESC_NAME} --type=merge -p '{ + "spec": { + "controllerConfig": { + "networkPolicies": [ + { + "name": "allow_egress", + "componentName": "ExternalSecretsCoreController", + "egress": [{"ports": [{"protocol": "TCP", "port": 6443}]}] + } + ] + } + } +}' 2>&1 | grep -i "invalid" + +# Test invalid name starting with hyphen (should fail) +oc patch externalsecretsconfig ${ESC_NAME} --type=merge -p '{ + "spec": { + "controllerConfig": { + "networkPolicies": [ + { + "name": "-allow-egress", + "componentName": "ExternalSecretsCoreController", + "egress": [{"ports": [{"protocol": "TCP", "port": 6443}]}] + } + ] + } + } +}' 2>&1 | grep -i "invalid" +``` + +## Step 5: Test Empty Egress (Deny-All) + +```bash +# Patch with empty egress for deny-all behavior +oc patch externalsecretsconfig ${ESC_NAME} --type=merge -p '{ + "spec": { + "controllerConfig": { + "networkPolicies": [ + { + "name": "deny-all-core-egress", + "componentName": "ExternalSecretsCoreController", + "egress": [] + } + ] + } + } +}' + +sleep 10 +oc get networkpolicy deny-all-core-egress -n ${OPERAND_NAMESPACE} -o yaml +``` + +## Step 6: Test Egress Update (Mutable) + +```bash +# Update the egress rules to add a new port +oc patch externalsecretsconfig ${ESC_NAME} --type=merge -p '{ + "spec": { + "controllerConfig": { + "networkPolicies": [ + { + "name": "allow-core-egress", + "componentName": "ExternalSecretsCoreController", + "egress": [ + {"ports": [{"protocol": "TCP", "port": 6443}]}, + {"ports": [{"protocol": "TCP", "port": 443}]} + ] + } + ] + } + } +}' + +sleep 10 +oc get networkpolicy allow-core-egress -n ${OPERAND_NAMESPACE} -o jsonpath='{.spec.egress}' | python3 -m json.tool +``` + +## Step 7: Test Invalid ComponentName + +```bash +# Should fail with Unsupported value +oc patch externalsecretsconfig ${ESC_NAME} --type=merge -p '{ + "spec": { + "controllerConfig": { + "networkPolicies": [ + { + "name": "test-policy", + "componentName": "InvalidComponent", + "egress": [{"ports": [{"protocol": "TCP", "port": 6443}]}] + } + ] + } + } +}' 2>&1 | grep -i "unsupported" +``` + +## Step 8: Cleanup Custom NetworkPolicies + +```bash +# Remove custom network policies by setting empty list +oc patch externalsecretsconfig ${ESC_NAME} --type=merge -p '{ + "spec": { + "controllerConfig": { + "networkPolicies": [] + } + } +}' + +sleep 10 + +# Verify custom policies were cleaned up +oc get networkpolicies -n ${OPERAND_NAMESPACE} -l operator.openshift.io/custom-network-policy=true +``` diff --git a/output/e2e_external-secrets-operator/test-cases.md b/output/e2e_external-secrets-operator/test-cases.md new file mode 100644 index 000000000..58418ff88 --- /dev/null +++ b/output/e2e_external-secrets-operator/test-cases.md @@ -0,0 +1,115 @@ +# E2E Test Cases: external-secrets-operator + +## Operator Information +- **Repository**: github.com/openshift/external-secrets-operator +- **Framework**: controller-runtime +- **API Group**: operator.openshift.io/v1alpha1 +- **Managed CRDs**: ExternalSecretsConfig, ExternalSecretsManager +- **Operator Namespace**: external-secrets-operator +- **Operand Namespace**: external-secrets +- **Changes Analyzed**: git diff origin/ai-staging-release-1.1...HEAD + +## Prerequisites +- OpenShift cluster with admin access +- `oc` CLI installed and authenticated +- External Secrets Operator installed via OLM or deployment + +## Changes Summary +The diff introduces NetworkPolicy API improvements to `ExternalSecretsConfig`: +1. DNS subdomain pattern validation on `networkPolicies[].name` field +2. Fixed `egress` JSON tag (removed `omitempty` for required field consistency) +3. Improved godoc for `ComponentName` constants and `egress` field +4. CRD schema updated with new `pattern` validation + +## Test Cases + +### TC-1: Create ExternalSecretsConfig with Custom NetworkPolicy for CoreController +- **Test**: Verify that creating an ExternalSecretsConfig with a custom network policy targeting ExternalSecretsCoreController creates a corresponding NetworkPolicy resource. +- **Steps**: + 1. Create ExternalSecretsConfig with `spec.controllerConfig.networkPolicies` containing a policy for `ExternalSecretsCoreController` + 2. Wait for Ready condition + 3. Verify NetworkPolicy exists in operand namespace with correct podSelector (`app.kubernetes.io/name: external-secrets`) + 4. Verify egress rules match the configured rules +- **Expected**: NetworkPolicy created with correct podSelector and egress rules + +### TC-2: Create ExternalSecretsConfig with Custom NetworkPolicy for BitwardenSDKServer +- **Test**: Verify that a custom network policy targeting BitwardenSDKServer creates a NetworkPolicy with the correct pod selector. +- **Steps**: + 1. Create ExternalSecretsConfig with a network policy for `BitwardenSDKServer` + 2. Verify NetworkPolicy exists with podSelector `app.kubernetes.io/name: bitwarden-sdk-server` +- **Expected**: NetworkPolicy created targeting bitwarden-sdk-server pods + +### TC-3: Validate NetworkPolicy Name DNS Subdomain Pattern +- **Test**: Verify that the `name` field in networkPolicies enforces DNS subdomain naming (lowercase alphanumeric and hyphens, no leading/trailing hyphens). +- **Steps**: + 1. Attempt to create ExternalSecretsConfig with `networkPolicies[].name` = "Allow-Egress" (uppercase) + 2. Attempt with name = "allow_egress" (underscore) + 3. Attempt with name = "-allow-egress" (leading hyphen) + 4. Attempt with name = "allow-egress-" (trailing hyphen) +- **Expected**: All four should be rejected with validation error + +### TC-4: Valid DNS Subdomain Names Accepted +- **Test**: Verify that valid DNS subdomain names are accepted for networkPolicies[].name. +- **Steps**: + 1. Create ExternalSecretsConfig with name = "allow-core-egress" + 2. Create with name = "a1b2c3" + 3. Create with name = "x" +- **Expected**: All valid names accepted without error + +### TC-5: NetworkPolicy with Empty Egress (Deny-All) +- **Test**: Verify that a NetworkPolicy with an empty egress list creates a deny-all egress policy. +- **Steps**: + 1. Create ExternalSecretsConfig with `egress: []` + 2. Verify NetworkPolicy created with empty egress rules +- **Expected**: NetworkPolicy exists with policyTypes: [Egress] and no egress rules (deny-all) + +### TC-6: Update Egress Rules in Existing NetworkPolicy +- **Test**: Verify that updating egress rules in an existing networkPolicy is allowed (name and componentName are immutable, but egress is mutable). +- **Steps**: + 1. Create ExternalSecretsConfig with a network policy allowing egress on port 6443 + 2. Update the same policy to also allow egress on port 443 + 3. Verify the NetworkPolicy resource is updated +- **Expected**: NetworkPolicy updated with new egress rules + +### TC-7: Immutability of Name and ComponentName +- **Test**: Verify that name and componentName fields in networkPolicies are immutable once set. +- **Steps**: + 1. Create ExternalSecretsConfig with a network policy + 2. Attempt to change the name or componentName +- **Expected**: Update rejected with immutability error + +### TC-8: Multiple NetworkPolicies +- **Test**: Verify that multiple custom network policies can be configured simultaneously. +- **Steps**: + 1. Create ExternalSecretsConfig with two network policies (one for CoreController, one for BitwardenSDKServer) + 2. Verify both NetworkPolicy resources exist +- **Expected**: Both NetworkPolicies created with correct selectors + +### TC-9: Static NetworkPolicies Exist +- **Test**: Verify that the operator creates static deny-all, DNS, and component-specific network policies. +- **Steps**: + 1. Deploy ExternalSecretsConfig with default spec + 2. List NetworkPolicies in operand namespace + 3. Verify deny-all, allow-dns, allow-main-controller, allow-webhook policies exist +- **Expected**: Static policies present, deny-all selects all pods + +### TC-10: Invalid ComponentName Rejected +- **Test**: Verify that an invalid componentName value is rejected at admission. +- **Steps**: + 1. Attempt to create ExternalSecretsConfig with componentName = "InvalidComponent" +- **Expected**: Rejected with "Unsupported value" error + +## Verification +```bash +oc get externalsecretsconfig cluster -o yaml +oc get networkpolicies -n external-secrets +oc describe networkpolicies -n external-secrets +oc get pods -n external-secrets-operator +oc logs -n external-secrets-operator -l app.kubernetes.io/name=external-secrets-operator --tail=50 +``` + +## Cleanup +```bash +oc delete externalsecretsconfig cluster --ignore-not-found +oc delete namespace external-secrets --ignore-not-found +```