diff --git a/docs/changelog.md b/docs/changelog.md index b1444fd..64b0d00 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,17 @@ All notable changes to this project will be documented in this file. -## [2.1.19] - Current +## [2.1.20] - Current + +### Fixed + +- **NetworkPolicy for CI egress**: The RHDH chart 2.y ships default-deny NetworkPolicies that only allow egress on specific ports (443, 53/5353, 5432, 6379). E2E tests need access to many other services (Keycloak HTTP routes, ArgoCD, external APIs), so we now apply a permissive `rhdh-allow-all-egress` NetworkPolicy after deployment. A TODO is kept to narrow this to explicit ports once all required destinations are mapped. + +### Added + +- **`applyNetworkPolicy` in `KubernetesClientHelper`**: New method to create or update NetworkPolicies using the Kubernetes client API (same pattern as ConfigMap/Secret). + +## [2.1.19] ### Changed diff --git a/package.json b/package.json index 3bc6d6b..6269799 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@red-hat-developer-hub/e2e-test-utils", - "version": "2.1.19", + "version": "2.1.20", "description": "Test utilities for RHDH E2E tests", "license": "Apache-2.0", "repository": { diff --git a/src/deployment/rhdh/deployment.ts b/src/deployment/rhdh/deployment.ts index e409335..a7d1b55 100644 --- a/src/deployment/rhdh/deployment.ts +++ b/src/deployment/rhdh/deployment.ts @@ -71,6 +71,7 @@ export class RHDHDeployment { await this._applyDynamicPlugins(); await this._deployWithOperator(this.deploymentConfig.subscription); } + await this._applyNetworkPolicies(); await this.waitUntilReady(); }, ); @@ -284,6 +285,49 @@ export class RHDHDeployment { this._log(`Helm deployment completed successfully`); } + /** + * Apply NetworkPolicies that the chart's default-deny rules don't cover. + * The chart ships default-deny NPs allowing egress only on specific ports + * (443, 53/5353, 5432, 6379). E2E tests talk to services on many other + * ports (Keycloak HTTP routes, ArgoCD, external APIs), so we allow all + * egress from the RHDH pods in CI. + */ + private async _applyNetworkPolicies(): Promise { + const namespace = this.deploymentConfig.namespace; + + const matchLabels: Record = + this.deploymentConfig.method === "operator" + ? // eslint-disable-next-line @typescript-eslint/naming-convention -- standard operator label; dots/slashes violate camelCase rule + { "rhdh.redhat.com/app": "backstage-developer-hub" } + : { + // eslint-disable-next-line @typescript-eslint/naming-convention -- standard Kubernetes recommended labels; dots/slashes violate camelCase rule + "app.kubernetes.io/name": "developer-hub", + // eslint-disable-next-line @typescript-eslint/naming-convention -- standard Kubernetes recommended labels; dots/slashes violate camelCase rule + "app.kubernetes.io/instance": "redhat-developer-hub", + // eslint-disable-next-line @typescript-eslint/naming-convention -- standard Kubernetes recommended labels; dots/slashes violate camelCase rule + "app.kubernetes.io/component": "backstage", + }; + + await this.k8sClient.applyNetworkPolicy( + { + apiVersion: "networking.k8s.io/v1", + kind: "NetworkPolicy", + metadata: { + name: "rhdh-allow-all-egress", + namespace, + }, + spec: { + // TODO: narrow to explicit ports once all required destinations are mapped + podSelector: { matchLabels }, + policyTypes: ["Egress"], + egress: [{}], + }, + }, + namespace, + ); + this._log("Applied NetworkPolicy: rhdh-allow-all-egress"); + } + private async _deployWithOperator(subscription: string): Promise { const subscriptionObject = (await mergeYamlFilesIfExists([ DEFAULT_CONFIG_PATHS.operator.subscription, diff --git a/src/utils/kubernetes-client.ts b/src/utils/kubernetes-client.ts index 542e448..07d3b98 100644 --- a/src/utils/kubernetes-client.ts +++ b/src/utils/kubernetes-client.ts @@ -13,6 +13,7 @@ class KubernetesClientHelper { private _kc: k8s.KubeConfig; private _k8sApi: k8s.CoreV1Api; private _appsApi: k8s.AppsV1Api; + private _networkingApi: k8s.NetworkingV1Api; private _customObjectsApi: k8s.CustomObjectsApi; constructor() { @@ -22,6 +23,7 @@ class KubernetesClientHelper { try { this._k8sApi = this._kc.makeApiClient(k8s.CoreV1Api); this._appsApi = this._kc.makeApiClient(k8s.AppsV1Api); + this._networkingApi = this._kc.makeApiClient(k8s.NetworkingV1Api); this._customObjectsApi = this._kc.makeApiClient(k8s.CustomObjectsApi); } catch (error) { if ( @@ -311,6 +313,37 @@ class KubernetesClientHelper { } } + async applyNetworkPolicy( + policy: k8s.V1NetworkPolicy, + namespace: string, + ): Promise { + const name = policy.metadata!.name!; + try { + await this._networkingApi.replaceNamespacedNetworkPolicy({ + name, + namespace, + body: policy, + }); + console.log(`✓ Updated NetworkPolicy ${name} in namespace ${namespace}`); + } catch { + try { + await this._networkingApi.createNamespacedNetworkPolicy({ + namespace, + body: policy, + }); + console.log( + `✓ Created NetworkPolicy ${name} in namespace ${namespace}`, + ); + } catch (createError) { + console.error( + `✗ Failed to create/update NetworkPolicy ${name} in namespace ${namespace}:`, + createError instanceof Error ? createError.message : createError, + ); + throw createError; + } + } + } + /** * Delete a namespace and wait for it to be fully terminated */