diff --git a/.claude/rules/api-types.md b/.claude/rules/api-types.md index daa7cd23e5..aceb843adc 100644 --- a/.claude/rules/api-types.md +++ b/.claude/rules/api-types.md @@ -1,11 +1,7 @@ ---- -paths: - - "api/**" ---- - # API Types Rules -- After modifying `api/v2/checluster_types.go`, always run `build/scripts/docker-run.sh make update-dev-resources` to regenerate CRDs, DeepCopy methods, and related manifests. +- After modifying `api/v2/checluster_types.go`, always run `make update-dev-resources` +(or `build/scripts/docker-run.sh make update-dev-resources` on macOS) to regenerate CRDs, DeepCopy methods, and related manifests. - v2 is the storage version. v1 exists only for conversion compatibility and does not need to be updated. - Webhook validation and defaulting logic live in `api/v2/checluster_webhook.go`. - `zz_generated.deepcopy.go` files are generated by `controller-gen` — never edit them manually. diff --git a/.claude/rules/build-and-test.md b/.claude/rules/build-and-test.md new file mode 100644 index 0000000000..ad7b3365b6 --- /dev/null +++ b/.claude/rules/build-and-test.md @@ -0,0 +1,10 @@ +# Build & Test Commands + +- Build: `make build` +- All unit tests: `make test` +- Single package: `MOCK_API=true go test -mod=vendor ./package/...` +- Single test: `MOCK_API=true go test -mod=vendor ./package/... -run TestSpecificName -v` +- Format: `make fmt` +- Vet: `make vet` +- Lint: `make lint` +- After making changes, always build and run the relevant tests before reporting the task as complete. diff --git a/.claude/rules/code-style.md b/.claude/rules/code-style.md new file mode 100644 index 0000000000..9b6855f4dc --- /dev/null +++ b/.claude/rules/code-style.md @@ -0,0 +1,7 @@ +# Code Style Rules + +- Add an empty line after logical blocks of code (e.g., after `if` blocks, loops, variable declaration groups) to improve readability. +- Wrap errors with context using `fmt.Errorf` at every call site instead of passing them through directly — this applies both inside the function and at the caller. Error messages must start with `failed to` (lowercase). When the object has a namespace, include it as `namespace/name` in the message. Use `return fmt.Errorf("failed to sync deployment %s/%s: %w", obj.Namespace, obj.Name, err)` instead of `return err`. +- Lines should not exceed 120 characters. If a function call exceeds this limit, put each argument on its own line. This does not apply to `fmt.Errorf`, `fmt.Sprintf`, and similar formatting functions — keep those on one line. +- Use `deploy.GetLabels(component)` for labeling resources in `pkg/deploy/` reconcilers. +- Do not use `logrus` for logging. Use the structured JSON logger (`ctrl.Log.WithName(...)`) already defined in the package. If none exists, add one as a package-level `var logger = ctrl.Log.WithName("package-name")`. diff --git a/.claude/rules/k8s-client.md b/.claude/rules/k8s-client.md new file mode 100644 index 0000000000..21eb054186 --- /dev/null +++ b/.claude/rules/k8s-client.md @@ -0,0 +1,9 @@ +# K8s Client Rules + +- Use the `K8sClient` wrapper (`pkg/common/k8s-client/`) for all Kubernetes operations. +- Access it via `DeployContext.ClusterAPI.ClientWrapper` (cached) or `DeployContext.ClusterAPI.NonCachingClientWrapper` (non-caching). +- **Do NOT use** the legacy functions from `pkg/deploy/sync.go` (`deploy.Sync`, `deploy.Get*`, `deploy.Delete*`, `deploy.CreateIgnoreIfExists`) — that file is deprecated. +- When using `Sync` to create/update an object in the **same namespace** as the CheCluster CR, set the owner reference before syncing: `controllerutil.SetControllerReference(ctx.CheCluster, obj, ctx.ClusterAPI.Scheme)`. Do **not** set owner references on objects in other namespaces (cross-namespace owner references are not supported by Kubernetes). +- When using `Sync`, always pass `&k8sclient.SyncOptions{DiffOpts: diffs.}` to control which fields are compared. Diff options live in `pkg/common/diffs/diffs.go`. If no diff option exists for the resource type, add one there first. +- When reading objects, use `ClientWrapper.GetIgnoreNotFound` — do **not** use `Client.Get`. `GetIgnoreNotFound` returns `(found bool, err error)` instead of returning a "not found" error. +- When deleting, use `DeleteByKeyIgnoreNotFound` for idempotent cleanup (e.g., toggling a feature off). diff --git a/.claude/rules/kubebuilder-defaults.md b/.claude/rules/kubebuilder-defaults.md new file mode 100644 index 0000000000..9591863b3d --- /dev/null +++ b/.claude/rules/kubebuilder-defaults.md @@ -0,0 +1,5 @@ +# Kubebuilder Default Propagation Rule + +- When adding a `+kubebuilder:default` annotation to a field, propagate the default up to every parent struct's `+kubebuilder:default` annotation as well. +- If the parent struct already has a `+kubebuilder:default`, add the new nested default to it. If it doesn't, add one. +- This is required because kubebuilder only applies a field's default when its parent object is non-nil. A pointer field (`*Foo`) with a default on its inner fields has no effect if the parent's default doesn't instantiate it. diff --git a/.claude/rules/pull-requests.md b/.claude/rules/pull-requests.md new file mode 100644 index 0000000000..4d89374829 --- /dev/null +++ b/.claude/rules/pull-requests.md @@ -0,0 +1,7 @@ +# Pull Request Rules + +- When creating a PR, use the project's PR template from `.github/PULL_REQUEST_TEMPLATE.md`. +- Fill in all sections: "What does this PR do?", "Screenshot/screencast of this PR", "What issues does this PR fix or reference?", and "How to test this PR?". +- Include deployment and test steps appropriate for the change (OpenShift via `test-catalog-from-sources.sh`, Minikube via `test-operator-from-sources.sh`, or both). +- Check all applicable items in the "Common Test Scenarios" checklist (deploy, start workspace, terminal, stop workspace, check operator logs). +- Check all applicable items in the "PR Checklist" (ECA, code complete, builds, tests, devfile, docs, CI/CD). diff --git a/.claude/rules/reconcilers.md b/.claude/rules/reconcilers.md index 61a08d5ca9..f38e7bdd88 100644 --- a/.claude/rules/reconcilers.md +++ b/.claude/rules/reconcilers.md @@ -1,14 +1,8 @@ ---- -paths: - - "pkg/deploy/**" - - "controllers/che/**" ---- - # Reconciler Rules - Every component reconciler implements the `Reconcilable` interface (`pkg/common/reconciler/reconcile_manager.go`): `Reconcile()` and `Finalize()`. -- `Reconcile()` returns `(result, done, err)` — return `done=true` only when the component is fully reconciled. -The pipeline stops at the first `done=false`. Use `RequeueAfter: time.Second` in `result` for error recovery and when dealing with cluster-scoped objects. +- `Reconcile()` returns `(result, done, err)` — return `done=true` only when the component is fully reconciled. The pipeline stops at the first `done=false`. Use `RequeueAfter: time.Second` in `result` for error recovery and when dealing with cluster-scoped objects. - Registration order in `controllers/che/checluster_controller.go` matters — reconcilers run sequentially and may depend on earlier ones having completed. - All reconcilers receive `DeployContext` (`pkg/common/chetypes/types.go`) — use its `ClusterAPI` clients for k8s operations, not standalone clients. - Some reconcilers are OpenShift-only (consolelink, container-capabilities) — guard with `infrastructure.IsOpenShift()`. +- The usernamespace controller (`controllers/usernamespace/`) is a separate controller, not a `Reconcilable` — it has its own `Reconcile(ctx, req)` method and manages per-user namespace resources. diff --git a/.claude/rules/redhat-compliance.md b/.claude/rules/redhat-compliance.md new file mode 100644 index 0000000000..bb36027374 --- /dev/null +++ b/.claude/rules/redhat-compliance.md @@ -0,0 +1,7 @@ +# Red Hat Compliance Rules + +- If a code suggestion matches known open-source code, include the original license text and copyright notice. Do not suggest code whose license compatibility with EPL-2.0 cannot be verified. +- When suggesting commit messages, include `Assisted-by: {AGENT_NAME}` trailer (e.g., `Assisted-by: Claude Opus 4.6`). +- Never include credentials, tokens, or secrets in code. +- All new Go files must include the EPL-2.0 copyright header (copy from any existing file in the repo). This does not apply to files in the `vendor/` directory. +- When editing an existing file, update the copyright year range end to the current year (e.g., `2019-2025` → `2019-2026`). diff --git a/.claude/rules/resource-naming.md b/.claude/rules/resource-naming.md new file mode 100644 index 0000000000..9bbac52f58 --- /dev/null +++ b/.claude/rules/resource-naming.md @@ -0,0 +1,4 @@ +# Resource Naming Rules + +- Resource names must not hardcode `eclipse-che`, `che`, or `devspaces`. Use `defaults.GetCheFlavor()` (from `pkg/common/operator-defaults`) to make names work for both upstream (`eclipse-che`/`che`) and downstream (`devspaces`). +- This applies to Kubernetes object names, label values, and any string embedded in resource specs that identifies the product. Constants like `constants.CheEclipseOrg` are fine — they are shared across both flavors. diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000000..b0c333cbf7 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,7 @@ +# Testing Rules + +- Use `test.NewCtxBuilder()` to create test `DeployContext` instances with a fake k8s client. +- Use `infrastructure.InitializeForTesting(infrastructure.OpenShiftV4)` or `infrastructure.Kubernetes` to set the platform. Use OpenShift when testing OpenShift-specific resources (e.g., network policies with `infrastructure.IsOpenShift()` guards). +- Use `defaults.InitializeForTesting("../../config/manager/manager.yaml")` in `init_test.go` (adjust relative path) to load operator defaults for `GetCheFlavor()` and other defaults. +- Tests are co-located with source files (`*_test.go`). Test helpers live in `pkg/common/test/`. +- Use `test.EnsureReconcile(t, ctx, reconciler.Reconcile)` to test reconcilers — it loops up to 10 iterations until `done=true` and fails if the reconciler never finishes. diff --git a/AGENTS.md b/AGENTS.md index 18ddbac05a..5d0abef984 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,56 +1,24 @@ # AGENTS.md -This file provides guidance to AI coding agents when working with code in this repository. +This file provides architectural context for AI coding agents working in this repository. +Actionable rules (build commands, code style, K8s client usage, compliance) live in `.claude/rules/`. ## Project Overview -Eclipse Che Operator — a Kubernetes/OpenShift operator built with Operator SDK and controller-runtime that manages -the lifecycle of Eclipse Che installations. It watches `CheCluster` custom resources and reconciles all Che components +Eclipse Che Operator — a Kubernetes/OpenShift operator built with Operator SDK and controller-runtime that manages +the lifecycle of Eclipse Che installations. It watches `CheCluster` custom resources and reconciles all Che components (dashboard, gateway, devfile registry, Che server, etc.). Go module: `github.com/eclipse-che/che-operator` -## Build & Test Commands - -```bash -# Build operator binary -make build - -# Run all unit tests -make test - -# Run a single test file or package -MOCK_API=true go test -mod=vendor ./controllers/che/... -run TestSpecificName -v - -# Run tests for a specific package -MOCK_API=true go test -mod=vendor ./pkg/deploy/gateway/... - -# Format code (uses goimports if available, falls back to go fmt) -make fmt - -# Run go vet -make vet - -# Run static code analyzers -make lint - -# Regenerate CRDs, DeepCopy methods, and related manifests -build/scripts/docker-run.sh make update-dev-resources - -# Build operator Docker image -make docker-build IMG= -``` - ## Architecture ### CRD & API Versions - **v2** (`api/v2/`) — current storage version. `CheClusterSpec` has top-level sections: `DevEnvironments`, `Components`, `GitServices`, `Networking`, `ContainerRegistry`. -- **v1** (`api/v1/`) — deprecated +- **v1** (`api/v1/`) — deprecated, exists only for conversion compatibility. - Webhooks (validation, defaulting, conversion) live in `api/v2/checluster_webhook.go`. -After modifying `api/v2/checluster_types.go`, run `build/scripts/docker-run.sh make update-dev-resources`. - ### Controller Structure **Entry point:** `cmd/main.go` — registers four controllers with the manager: @@ -62,24 +30,18 @@ After modifying `api/v2/checluster_types.go`, run `build/scripts/docker-run.sh m ### Reconciliation Pipeline -`CheClusterReconciler` uses a `ReconcilerManager` (`pkg/common/reconciler/`) that runs a chain of -`Reconcilable` implementations **in order**. Each reconciler returns `(result, done, err)` — the chain stops -at the first `done=false`. Registration order in `controllers/che/checluster_controller.go` defines the execution order: +`CheClusterReconciler` uses a `ReconcilerManager` (`pkg/common/reconciler/`) that runs a chain of +`Reconcilable` implementations **in order**. Each reconciler returns `(result, done, err)` — the chain stops +at the first `done=false`. Registration order in `controllers/che/checluster_controller.go` defines the execution order. ### DeployContext `DeployContext` (`pkg/common/chetypes/types.go`) is the central context object passed to every reconciler. It carries: - `CheCluster` — the CR being reconciled -- `ClusterAPI` — cached and non-cached k8s clients, discovery client +- `ClusterAPI` — cached and non-cached k8s clients, discovery client, `ClientWrapper` / `NonCachingClientWrapper` - `Proxy`, `Authentication` — resolved configuration - `IsSelfSignedCertificate`, `CheHost`, `DwoNamespace` -### Kubernetes Client Usage - -Use the `K8sClient` wrapper (`pkg/common/k8s-client/`) for all Kubernetes operations. Access it via `DeployContext.ClusterAPI.ClientWrapper` (cached) or `DeployContext.ClusterAPI.NonCachingClientWrapper` (non-caching, for cluster-scoped or cross-namespace objects). Key methods: `Sync`, `Create`, `CreateIfNotExists`, `GetIgnoreNotFound`, `DeleteByKeyIgnoreNotFound`, `List`. - -**Do NOT use** the legacy functions from `pkg/deploy/sync.go` (`deploy.Sync`, `deploy.Get*`, `deploy.Delete*`, `deploy.CreateIgnoreIfExists`, etc.) — that file is deprecated. - ### Component Packages (`pkg/deploy/`) Each Che component has its own package under `pkg/deploy/` (e.g., `dashboard/`, `gateway/`, `postgres/`, `identity-provider/`, `server/`). Each package typically contains: @@ -89,12 +51,12 @@ Each Che component has its own package under `pkg/deploy/` (e.g., `dashboard/`, ### Platform Detection -`pkg/common/infrastructure/` detects Kubernetes vs OpenShift and feature availability (OAuth, image puller, service monitors). +`pkg/common/infrastructure/` detects Kubernetes vs OpenShift and feature availability (OAuth, image puller, service monitors). Some reconcilers are conditionally registered based on platform (e.g., `ConsoleLink` and `ContainerCapabilities` are OpenShift-only). ### Operator Defaults -`pkg/common/operator-defaults/` reads default container images and configuration from environment variables. +`pkg/common/operator-defaults/` reads default container images and configuration from environment variables. Resource limit/request defaults live in `pkg/common/constants/`. ### Testing @@ -119,13 +81,3 @@ build/scripts/olm/test-catalog-from-sources.sh # Minikube build/scripts/minikube-tests/test-operator-from-sources.sh ``` - -## Code Style - -- Add an empty line after logical blocks of code (e.g., after `if` blocks, loops, variable declaration groups) to improve readability. -- Wrap errors with context using `fmt.Errorf` instead of passing them through directly. For example, use `return fmt.Errorf("failed to sync deployment: %w", err)` instead of `return err`. -- After modifying `api/v2/checluster_types.go`, run `make update-dev-resources` to regenerate CRDs, DeepCopy methods, and related manifests. On macOS, use `build/scripts/docker-run.sh make update-dev-resources` instead. - -## Red Hat Compliance and Responsible AI Rules - -See [redhat-compliance-and-responsible-ai.md](redhat-compliance-and-responsible-ai.md) and the Cursor rules file under `.cursor/rules/`. diff --git a/api/v2/checluster_types.go b/api/v2/checluster_types.go index b7ccbc1896..58774ce70d 100644 --- a/api/v2/checluster_types.go +++ b/api/v2/checluster_types.go @@ -329,6 +329,25 @@ type CheClusterSpecNetworking struct { // +optional // +kubebuilder:default:={gateway: {configLabels: {app: che, component: che-gateway-config}}} Auth Auth `json:"auth"` + // NetworkPolicy configures NetworkPolicy resources for the Che namespace + // and user workspace namespaces. For OpenShift clusters only. + // When enabled, the following policies are created: + // In the Che namespace: + // - allow-from-same-namespace: allows ingress traffic between Che pods in the same namespace. + // - allow-from-workspaces: allows ingress traffic from user workspace namespaces. + // - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + // - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + // - allow-from--operator: allows ingress traffic from the operator pod to Che components. + // - allow-all-egress: allows all egress traffic from Che pods. + // In each user workspace namespace: + // - allow-from-: allows ingress traffic from the Che namespace. + // - allow-from-same-namespace: allows ingress traffic between pods in the same namespace. + // - allow-from-devworkspace-operator: allows ingress traffic from the DevWorkspace operator. + // - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + // - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + // - allow-all-egress: allows all egress traffic from workspace pods. + // +optional + NetworkPolicy *NetworkPolicy `json:"networkPolicy,omitempty"` } type DevEnvironmentNetworking struct { @@ -337,6 +356,14 @@ type DevEnvironmentNetworking struct { ExternalTLSConfig *ExternalTLSConfig `json:"externalTLSConfig,omitempty"` } +// NetworkPolicy configuration settings. +// +k8s:openapi-gen=true +type NetworkPolicy struct { + // Enabled controls whether the operator creates NetworkPolicy resources. + // +optional + Enabled *bool `json:"enabled,omitempty"` +} + type ExternalTLSConfig struct { // Enabled determines whether external TLS configuration is used. // If set to true, the operator will not set TLS config for ingress/route objects. @@ -1259,3 +1286,8 @@ func (c *CheCluster) IsDevEnvironmentExternalTLSConfigEnabled() bool { c.Spec.DevEnvironments.Networking.ExternalTLSConfig != nil && *c.Spec.DevEnvironments.Networking.ExternalTLSConfig.Enabled } + +func (c *CheCluster) IsNetworkPoliciesEnabled() bool { + return c.Spec.Networking.NetworkPolicy != nil && + ptr.Deref(c.Spec.Networking.NetworkPolicy.Enabled, constants.NetworkPolicyEnabled) +} diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 37951e91c1..45adecde6c 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -521,6 +521,11 @@ func (in *CheClusterSpecNetworking) DeepCopyInto(out *CheClusterSpecNetworking) } } in.Auth.DeepCopyInto(&out.Auth) + if in.NetworkPolicy != nil { + in, out := &in.NetworkPolicy, &out.NetworkPolicy + *out = new(NetworkPolicy) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheClusterSpecNetworking. @@ -1071,6 +1076,26 @@ func (in *KubeRbacProxy) DeepCopy() *KubeRbacProxy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkPolicy) DeepCopyInto(out *NetworkPolicy) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkPolicy. +func (in *NetworkPolicy) DeepCopy() *NetworkPolicy { + if in == nil { + return nil + } + out := new(NetworkPolicy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OAuthProxy) DeepCopyInto(out *OAuthProxy) { *out = *in diff --git a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml index 5cb5302d31..06b42dd7d5 100644 --- a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml +++ b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml @@ -86,7 +86,7 @@ metadata: categories: Developer Tools certified: "false" containerImage: quay.io/eclipse/che-operator:next - createdAt: "2026-07-24T14:28:09Z" + createdAt: "2026-07-30T08:51:48Z" description: A Kube-native development solution that delivers portable and collaborative developer workspaces. features.operators.openshift.io/cnf: "false" @@ -108,7 +108,7 @@ metadata: operatorframework.io/arch.amd64: supported operatorframework.io/arch.arm64: supported operatorframework.io/os.linux: supported - name: eclipse-che.v7.121.0-1044.next + name: eclipse-che.v7.121.0-1053.next namespace: placeholder spec: apiservicedefinitions: {} @@ -1173,7 +1173,7 @@ spec: name: openvsx - image: quay.io/sclorg/postgresql-16-c9s:20260319 name: openvsx-postgres - version: 7.121.0-1044.next + version: 7.121.0-1053.next webhookdefinitions: - admissionReviewVersions: - v1 diff --git a/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml b/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml index dfe7f209a4..5ac72a547c 100644 --- a/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml +++ b/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml @@ -27968,6 +27968,31 @@ spec: description: Defines labels which will be set for an Ingress (a route for OpenShift platform). type: object + networkPolicy: + description: |- + NetworkPolicy configures NetworkPolicy resources for the Che namespace + and user workspace namespaces. For OpenShift clusters only. + When enabled, the following policies are created: + In the Che namespace: + - allow-from-same-namespace: allows ingress traffic between Che pods in the same namespace. + - allow-from-workspaces: allows ingress traffic from user workspace namespaces. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from--operator: allows ingress traffic from the operator pod to Che components. + - allow-all-egress: allows all egress traffic from Che pods. + In each user workspace namespace: + - allow-from-: allows ingress traffic from the Che namespace. + - allow-from-same-namespace: allows ingress traffic between pods in the same namespace. + - allow-from-devworkspace-operator: allows ingress traffic from the DevWorkspace operator. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-all-egress: allows all egress traffic from workspace pods. + properties: + enabled: + description: Enabled controls whether the operator creates + NetworkPolicy resources. + type: boolean + type: object tlsSecretName: description: |- The name of the secret used to set up Ingress TLS termination. diff --git a/cmd/main.go b/cmd/main.go index eb2a860734..af9f5cc12b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -52,6 +52,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/selection" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -335,73 +336,79 @@ func main() { // watch k8s objects with labels `app.kubernetes.io/part-of=che.eclipse.org` func getCacheFunc() (cache.NewCacheFunc, error) { - partOfCheObjectSelector, err := labels.Parse(fmt.Sprintf("%s=%s", constants.KubernetesPartOfLabelKey, constants.CheEclipseOrg)) + partOfEclipseChe := labels.SelectorFromSet(map[string]string{constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg}) + + partOfCheOrDWO, err := labels.NewRequirement( + constants.KubernetesPartOfLabelKey, + selection.In, + []string{constants.CheEclipseOrg, constants.DevWorkspaceOperatorName}, + ) if err != nil { return nil, err } selectors := map[client.Object]cache.ByObject{ &appsv1.Deployment{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &corev1.Pod{}: { - Label: partOfCheObjectSelector, + Label: labels.NewSelector().Add(*partOfCheOrDWO), }, &batchv1.Job{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &corev1.Service{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &networkingv1.Ingress{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &networkingv1.NetworkPolicy{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &corev1.ConfigMap{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &corev1.Secret{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &corev1.ServiceAccount{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &rbacv1.Role{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &rbacv1.RoleBinding{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &rbacv1.ClusterRole{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &rbacv1.ClusterRoleBinding{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &corev1.PersistentVolumeClaim{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &corev1.LimitRange{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, &corev1.ResourceQuota{}: { - Label: partOfCheObjectSelector, + Label: partOfEclipseChe, }, } if infrastructure.IsOpenShift() { - selectors[&routev1.Route{}] = cache.ByObject{Label: partOfCheObjectSelector} - selectors[&templatev1.Template{}] = cache.ByObject{Label: partOfCheObjectSelector} + selectors[&routev1.Route{}] = cache.ByObject{Label: partOfEclipseChe} + selectors[&templatev1.Template{}] = cache.ByObject{Label: partOfEclipseChe} } if infrastructure.IsServiceMonitorEnabled() { - selectors[&monitoringv1.ServiceMonitor{}] = cache.ByObject{Label: partOfCheObjectSelector} + selectors[&monitoringv1.ServiceMonitor{}] = cache.ByObject{Label: partOfEclipseChe} } if infrastructure.IsOpenShiftOAuthEnabled() { - selectors[&oauthv1.OAuthClient{}] = cache.ByObject{Label: partOfCheObjectSelector} + selectors[&oauthv1.OAuthClient{}] = cache.ByObject{Label: partOfEclipseChe} } return func(config *rest.Config, opts cache.Options) (cache.Cache, error) { diff --git a/config/crd/bases/org.eclipse.che_checlusters.yaml b/config/crd/bases/org.eclipse.che_checlusters.yaml index 8b97ae7aae..943c105574 100644 --- a/config/crd/bases/org.eclipse.che_checlusters.yaml +++ b/config/crd/bases/org.eclipse.che_checlusters.yaml @@ -27842,6 +27842,31 @@ spec: description: Defines labels which will be set for an Ingress (a route for OpenShift platform). type: object + networkPolicy: + description: |- + NetworkPolicy configures NetworkPolicy resources for the Che namespace + and user workspace namespaces. For OpenShift clusters only. + When enabled, the following policies are created: + In the Che namespace: + - allow-from-same-namespace: allows ingress traffic between Che pods in the same namespace. + - allow-from-workspaces: allows ingress traffic from user workspace namespaces. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from--operator: allows ingress traffic from the operator pod to Che components. + - allow-all-egress: allows all egress traffic from Che pods. + In each user workspace namespace: + - allow-from-: allows ingress traffic from the Che namespace. + - allow-from-same-namespace: allows ingress traffic between pods in the same namespace. + - allow-from-devworkspace-operator: allows ingress traffic from the DevWorkspace operator. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-all-egress: allows all egress traffic from workspace pods. + properties: + enabled: + description: Enabled controls whether the operator creates + NetworkPolicy resources. + type: boolean + type: object tlsSecretName: description: |- The name of the secret used to set up Ingress TLS termination. diff --git a/controllers/che/checluster_controller.go b/controllers/che/checluster_controller.go index af83202c78..0a95654e4f 100644 --- a/controllers/che/checluster_controller.go +++ b/controllers/che/checluster_controller.go @@ -14,12 +14,14 @@ package che import ( "context" + "fmt" "time" "github.com/eclipse-che/che-operator/pkg/common/constants" k8sclient "github.com/eclipse-che/che-operator/pkg/common/k8s-client" "github.com/eclipse-che/che-operator/pkg/common/reconciler" "github.com/eclipse-che/che-operator/pkg/deploy/metrics" + "github.com/eclipse-che/che-operator/pkg/deploy/networkpolicies" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -110,6 +112,10 @@ func NewReconciler( reconcilerManager.AddReconciler(devworkspace.NewDevWorkspaceConfigReconciler()) reconcilerManager.AddReconciler(rbac.NewGatewayPermissionsReconciler()) + if infrastructure.IsOpenShift() { + reconcilerManager.AddReconciler(networkpolicies.NewNetworkPoliciesReconciler()) + } + // we have to expose che endpoint independently of syncing other server // resources since che host is used for dashboard deployment and che config map reconcilerManager.AddReconciler(server.NewCheHostReconciler()) @@ -124,7 +130,6 @@ func NewReconciler( reconcilerManager.AddReconciler(openvsxdatabase.NewOpenVSXDatabaseReconciler()) reconcilerManager.AddReconciler(openvsxserver.NewOpenVSXServerReconciler()) reconcilerManager.AddReconciler(editorsdefinitions.NewEditorsDefinitionsReconciler()) - reconcilerManager.AddReconciler(devworkspace.NewDwoNamespaceReconciler()) reconcilerManager.AddReconciler(dashboard.NewDashboardReconciler()) reconcilerManager.AddReconciler(gateway.NewGatewayReconciler()) reconcilerManager.AddReconciler(server.NewCheServerReconciler()) @@ -178,6 +183,7 @@ func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&rbacv1.RoleBinding{}). Owns(&corev1.ServiceAccount{}). Owns(&appsv1.Deployment{}). + Owns(&networking.NetworkPolicy{}). Watches(&corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(toTrustedBundleConfigMapRequestMapper), ). @@ -240,31 +246,38 @@ func (r *CheClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) deployContext := &chetypes.DeployContext{ ClusterAPI: clusterAPI, CheCluster: checluster, + Context: ctx, } // Resolve proxy configuration - proxy, err := GetProxyConfiguration(deployContext) + deployContext.Proxy, err = GetProxyConfiguration(deployContext) if err != nil { r.Log.Error(err, "Error on reading proxy configuration") return ctrl.Result{}, err } - deployContext.Proxy = proxy // Resolve authentication configuration - authentication, err := ResolveAuthentication(deployContext) + deployContext.Authentication, err = ResolveAuthentication(deployContext) if err != nil { r.Log.Error(err, "Error on resolving authentication") return ctrl.Result{}, err } - deployContext.Authentication = authentication + + deployContext.DWONamespace, err = devworkspace.GetDevWorkspaceOperatorNamespace(ctx, clusterAPI.ClientWrapper) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get DevWorkspaceOperator namespace: %w", err) + } + if deployContext.DWONamespace == "" { + r.Log.Info("DevWorkspaceOperator namespace not found, requeuing.") + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } // Detect whether self-signed certificate is used - isSelfSignedCertificate, err := tls.IsSelfSignedCertificateUsed(deployContext) + deployContext.IsSelfSignedCertificate, err = tls.IsSelfSignedCertificateUsed(deployContext) if err != nil { r.Log.Error(err, "Failed to detect if self-signed certificate used.") return ctrl.Result{}, err } - deployContext.IsSelfSignedCertificate = isSelfSignedCertificate if deployContext.CheCluster.DeletionTimestamp.IsZero() { result, done, err := r.reconcilerManager.ReconcileAll(deployContext) diff --git a/controllers/usernamespace/usernamespace_controller.go b/controllers/usernamespace/usernamespace_controller.go index d142e7583c..1b1dea5a59 100644 --- a/controllers/usernamespace/usernamespace_controller.go +++ b/controllers/usernamespace/usernamespace_controller.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // This program and the accompanying materials are made // available under the terms of the Eclipse Public License 2.0 // which is available at https://www.eclipse.org/legal/epl-2.0/ @@ -18,12 +18,17 @@ import ( "fmt" "strconv" "strings" + "sync" "github.com/eclipse-che/che-operator/pkg/common/diffs" + "github.com/eclipse-che/che-operator/pkg/common/infrastructure" k8sclient "github.com/eclipse-che/che-operator/pkg/common/k8s-client" + defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" containercapabilties "github.com/eclipse-che/che-operator/pkg/deploy/container-capabilities" + "github.com/eclipse-che/che-operator/pkg/deploy/networkpolicies" "k8s.io/utils/ptr" + devworkspacedefaults "github.com/eclipse-che/che-operator/controllers/devworkspace/defaults" "github.com/eclipse-che/che-operator/controllers/namespacecache" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -36,12 +41,11 @@ import ( dwconstants "github.com/devfile/devworkspace-operator/pkg/constants" chev2 "github.com/eclipse-che/che-operator/api/v2" "github.com/eclipse-che/che-operator/controllers/che" - "github.com/eclipse-che/che-operator/controllers/devworkspace/defaults" - "github.com/eclipse-che/che-operator/pkg/common/infrastructure" "github.com/eclipse-che/che-operator/pkg/deploy" projectv1 "github.com/openshift/api/project/v1" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -60,6 +64,10 @@ const ( podTolerationsAnnotation = "controller.devfile.io/pod-tolerations" ) +var ( + logger = ctrl.Log.WithName("usernamespace") +) + type CheUserNamespaceReconciler struct { scheme *runtime.Scheme client client.Client @@ -67,6 +75,9 @@ type CheUserNamespaceReconciler struct { clientWrapper *k8sclient.K8sClientWrapper nonCachedClientWrapper *k8sclient.K8sClientWrapper namespaceCache *namespacecache.NamespaceCache + + dwoNamespace string + dwoNamespaceMu sync.RWMutex } var _ reconcile.Reconciler = (*CheUserNamespaceReconciler)(nil) @@ -98,9 +109,14 @@ func (r *CheUserNamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error { ctx := context.Background() bld := ctrl.NewControllerManagedBy(mgr). For(obj). + // The DWO namespace is cached to avoid a cluster-wide pod list on every reconcile. + // This watch invalidates the cache when the DWO pod is (re)created, + // so the allow-from-devworkspace-operator network policy stays up to date. + Watches(&corev1.Pod{}, r.watchRuleForDevWorkspacePod()). Watches(&corev1.Secret{}, r.watchRulesForSecrets(ctx)). Watches(&corev1.ConfigMap{}, r.watchRulesForConfigMaps(ctx)). - Watches(&chev2.CheCluster{}, r.triggerAllNamespaces()) + Watches(&chev2.CheCluster{}, r.triggerAllNamespaces()). + Watches(&networkingv1.NetworkPolicy{}, r.watchRulesForNetworkPolicies(ctx)) // Use controller.TypedOptions to allow to configure 2 controllers for same object being reconciled return bld.WithOptions( @@ -135,6 +151,32 @@ func (r *CheUserNamespaceReconciler) commonRules(ctx context.Context, namesInChe } } +func (r *CheUserNamespaceReconciler) watchRulesForNetworkPolicies(_ context.Context) handler.EventHandler { + return handler.EnqueueRequestsFromMapFunc( + func(ctx context.Context, obj client.Object) []reconcile.Request { + workspaceInfo, err := r.namespaceCache.GetNamespaceInfo(ctx, obj.GetNamespace()) + if err != nil { + logger.Error(err, "failed to get namespace info", "Namespace", obj.GetNamespace()) + return []reconcile.Request{} + } + + if workspaceInfo != nil && + workspaceInfo.IsWorkspaceNamespace && + deploy.IsOperatorManagedComponent(obj.GetLabels(), defaults.GetCheFlavor()) { + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: obj.GetNamespace(), + }, + }, + } + } + + return []reconcile.Request{} + }) +} + func (r *CheUserNamespaceReconciler) watchRulesForConfigMaps(ctx context.Context) handler.EventHandler { rules := r.commonRules(ctx, tls.CheMergedCABundleCertsCMName) return handler.EnqueueRequestsFromMapFunc( @@ -154,7 +196,7 @@ func (r *CheUserNamespaceReconciler) hasNameAndIsCollocatedWithCheCluster(ctx co } func isLabeledAsUserSettings(obj metav1.Object) bool { - return obj.GetLabels()["app.kubernetes.io/component"] == userSettingsComponentLabelValue + return obj.GetLabels()[constants.KubernetesComponentLabelKey] == userSettingsComponentLabelValue } func (r *CheUserNamespaceReconciler) isInManagedNamespace(ctx context.Context, obj metav1.Object) bool { @@ -166,7 +208,7 @@ func (r *CheUserNamespaceReconciler) triggerAllNamespaces() handler.EventHandler return handler.EnqueueRequestsFromMapFunc( handler.MapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { nss := r.namespaceCache.GetAllKnownNamespaces() - ret := make([]reconcile.Request, len(nss)) + ret := make([]reconcile.Request, 0, len(nss)) for _, ns := range nss { ret = append(ret, reconcile.Request{ @@ -216,8 +258,11 @@ func (r *CheUserNamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Req ClusterAPI: chetypes.ClusterAPI{ Client: r.client, NonCachingClient: r.nonCachedClient, + ClientWrapper: r.clientWrapper, Scheme: r.scheme, }, + Context: ctx, + DWONamespace: r.getDWONamespace(), } // Deprecated [CRW-6792]. @@ -277,6 +322,12 @@ func (r *CheUserNamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, err } + if infrastructure.IsOpenShift() { + if err = r.reconcileNetworkPolicies(deployContext, req.Name); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to reconcile network policies in namespace %s: %w", req.Name, err) + } + } + return ctrl.Result{}, nil } @@ -313,7 +364,7 @@ func (r *CheUserNamespaceReconciler) reconcileSelfSignedCert(ctx context.Context ObjectMeta: metav1.ObjectMeta{ Name: targetCertName, Namespace: targetNs, - Labels: defaults.AddStandardLabelsForComponent(checluster, userSettingsComponentLabelValue, map[string]string{ + Labels: devworkspacedefaults.AddStandardLabelsForComponent(checluster, userSettingsComponentLabelValue, map[string]string{ dwconstants.DevWorkspaceMountLabel: "true", dwconstants.DevWorkspaceWatchSecretLabel: "true", }), @@ -375,7 +426,7 @@ func (r *CheUserNamespaceReconciler) reconcileUserSettings( annotations := map[string]string{ dwconstants.DevWorkspaceMountAsAnnotation: "env", } - labels := defaults.AddStandardLabelsForComponent(checluster, + labels := devworkspacedefaults.AddStandardLabelsForComponent(checluster, userSettingsComponentLabelValue, map[string]string{ dwconstants.DevWorkspaceMountLabel: "true", @@ -473,7 +524,7 @@ func (r *CheUserNamespaceReconciler) reconcileGitTlsCertificate(ctx context.Cont ObjectMeta: metav1.ObjectMeta{ Name: targetName, Namespace: targetNs, - Labels: defaults.AddStandardLabelsForComponent(checluster, userSettingsComponentLabelValue, map[string]string{ + Labels: devworkspacedefaults.AddStandardLabelsForComponent(checluster, userSettingsComponentLabelValue, map[string]string{ dwconstants.DevWorkspaceGitTLSLabel: "true", dwconstants.DevWorkspaceMountLabel: "true", dwconstants.DevWorkspaceWatchConfigMapLabel: "true", @@ -590,6 +641,24 @@ func (r *CheUserNamespaceReconciler) reconcileSCCPrivileges( ) } +func (r *CheUserNamespaceReconciler) reconcileNetworkPolicies(deployContext *chetypes.DeployContext, targetNs string) error { + if !deployContext.CheCluster.IsNetworkPoliciesEnabled() { + err := networkpolicies.DeleteNetworkPolicy(deployContext, targetNs) + if err != nil { + err = fmt.Errorf("failed to delete NetworkPolicy in namespace %s: %w", targetNs, err) + } + + return err + } + + err := networkpolicies.SyncNetworkPolicy(deployContext, targetNs) + if err != nil { + return fmt.Errorf("failed to sync NetworkPolicy in namespace %s: %w", targetNs, err) + } + + return nil +} + func prefixedName(name string) string { return "che-" + name } diff --git a/controllers/usernamespace/usernamespace_controller_dwo_namespace.go b/controllers/usernamespace/usernamespace_controller_dwo_namespace.go new file mode 100644 index 0000000000..b8a9a2e240 --- /dev/null +++ b/controllers/usernamespace/usernamespace_controller_dwo_namespace.go @@ -0,0 +1,116 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + "context" + + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/eclipse-che/che-operator/pkg/deploy/devworkspace" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func (r *CheUserNamespaceReconciler) watchRuleForDevWorkspacePod() handler.EventHandler { + // Handle the create event to discover the DevWorkspace Operator namespace. + // Log an error if multiple DevWorkspace Operators are found running in different namespaces, as this indicates an invalid deployment. + return handler.Funcs{ + CreateFunc: func( + ctx context.Context, + evt event.CreateEvent, + q workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + pod, ok := evt.Object.(*corev1.Pod) + if !ok { + return + } + + if !deploy.IsDevWorkspaceComponent(pod.GetLabels()) || + pod.Spec.ServiceAccountName != constants.DevWorkspaceServiceAccountName { + return + } + + newNamespace := pod.GetNamespace() + currentNamespace := r.getDWONamespace() + + if currentNamespace == newNamespace { + return + } + + r.setDWONamespace(newNamespace) + + for _, namespace := range r.namespaceCache.GetAllKnownNamespaces() { + q.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{Name: namespace}, + }) + } + }, + // Handle the delete event to resolve an invalid configuration where + // multiple DevWorkspace Operators are running in different namespaces. + DeleteFunc: func( + ctx context.Context, + evt event.DeleteEvent, + q workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + pod, ok := evt.Object.(*corev1.Pod) + if !ok { + return + } + + if !deploy.IsDevWorkspaceComponent(pod.GetLabels()) || + pod.Spec.ServiceAccountName != constants.DevWorkspaceServiceAccountName { + return + } + + currentDWONamespace := r.getDWONamespace() + + // Find the new DWO namespace + newDWONamespace, err := devworkspace.GetDevWorkspaceOperatorNamespace(ctx, r.clientWrapper) + if err != nil { + logger.Error(err, "Failed to get DevWorkspaceOperator namespace") + return + } + + if currentDWONamespace == newDWONamespace { + return + } + + r.setDWONamespace(newDWONamespace) + + for _, namespace := range r.namespaceCache.GetAllKnownNamespaces() { + q.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{Name: namespace}, + }) + } + }, + } +} + +func (r *CheUserNamespaceReconciler) setDWONamespace(dwoNamespace string) { + r.dwoNamespaceMu.Lock() + defer r.dwoNamespaceMu.Unlock() + + r.dwoNamespace = dwoNamespace +} + +func (r *CheUserNamespaceReconciler) getDWONamespace() string { + r.dwoNamespaceMu.RLock() + defer r.dwoNamespaceMu.RUnlock() + + return r.dwoNamespace +} diff --git a/controllers/usernamespace/usernamespace_controller_network_policies_test.go b/controllers/usernamespace/usernamespace_controller_network_policies_test.go new file mode 100644 index 0000000000..4a4f3b8e5d --- /dev/null +++ b/controllers/usernamespace/usernamespace_controller_network_policies_test.go @@ -0,0 +1,199 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + "context" + "testing" + + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/common/infrastructure" + projectv1 "github.com/openshift/api/project/v1" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var allUserNsNetworkPolicyNames = []string{ + "allow-from-eclipse-che", + "allow-from-same-namespace", + "allow-from-devworkspace-operator", + "allow-from-openshift-monitoring", + "allow-from-openshift-ingress", + "allow-all-egress", +} + +func TestNetworkPoliciesCreatedWhenEnabledOnOpenShift(t *testing.T) { + cheCluster, userNamespace, userProject := buildNetworkPolicyTestObjects() + _, cl, r := setup(infrastructure.OpenShiftV4, cheCluster, userNamespace, userProject) + + _, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "user-project"}}) + assert.NoError(t, err) + + npList := &networkingv1.NetworkPolicyList{} + err = cl.List(context.TODO(), npList, &client.ListOptions{Namespace: "user-project"}) + assert.NoError(t, err) + assert.Equal(t, len(allUserNsNetworkPolicyNames), len(npList.Items)) + + for _, name := range allUserNsNetworkPolicyNames { + np := &networkingv1.NetworkPolicy{} + err := cl.Get( + context.TODO(), + types.NamespacedName{Name: name, Namespace: "user-project"}, + np, + ) + assert.NoError(t, err) + } +} + +func TestNetworkPoliciesDeletedWhenDisabled(t *testing.T) { + cheCluster, userNamespace, userProject := buildNetworkPolicyTestObjects() + _, cl, r := setup(infrastructure.OpenShiftV4, cheCluster, userNamespace, userProject) + + _, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "user-project"}}) + assert.NoError(t, err) + + cheCluster.Spec.Networking.NetworkPolicy.Enabled = ptr.To(false) + err = cl.Update(context.TODO(), cheCluster) + assert.NoError(t, err) + + _, err = r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "user-project"}}) + assert.NoError(t, err) + + for _, name := range allUserNsNetworkPolicyNames { + np := &networkingv1.NetworkPolicy{} + err := cl.Get( + context.TODO(), + types.NamespacedName{Name: name, Namespace: "user-project"}, + np, + ) + assert.Error(t, err) + assert.True(t, errors.IsNotFound(err)) + } +} + +func TestNetworkPoliciesIdempotent(t *testing.T) { + cheCluster, userNamespace, userProject := buildNetworkPolicyTestObjects() + _, cl, r := setup(infrastructure.OpenShiftV4, cheCluster, userNamespace, userProject) + + _, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "user-project"}}) + assert.NoError(t, err) + + _, err = r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "user-project"}}) + assert.NoError(t, err) + + for _, name := range allUserNsNetworkPolicyNames { + np := &networkingv1.NetworkPolicy{} + err := cl.Get( + context.TODO(), + types.NamespacedName{Name: name, Namespace: "user-project"}, + np, + ) + assert.NoError(t, err) + } +} + +func TestNetworkPoliciesDeletedOnlyOwnedPolicies(t *testing.T) { + cheCluster, userNamespace, userProject := buildNetworkPolicyTestObjects() + + unownedNetworkPolicy := &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unowned-policy", + Namespace: "user-project", + }, + } + + _, cl, r := setup( + infrastructure.OpenShiftV4, + cheCluster, + userNamespace, + userProject, + unownedNetworkPolicy, + ) + + _, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "user-project"}}) + assert.NoError(t, err) + + cheCluster.Spec.Networking.NetworkPolicy.Enabled = ptr.To(false) + err = cl.Update(context.TODO(), cheCluster) + assert.NoError(t, err) + + _, err = r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "user-project"}}) + assert.NoError(t, err) + + for _, name := range allUserNsNetworkPolicyNames { + np := &networkingv1.NetworkPolicy{} + err := cl.Get( + context.TODO(), + types.NamespacedName{Name: name, Namespace: "user-project"}, + np, + ) + assert.Error(t, err) + assert.True(t, errors.IsNotFound(err)) + } + + np := &networkingv1.NetworkPolicy{} + err = cl.Get( + context.TODO(), + types.NamespacedName{Name: "unowned-policy", Namespace: "user-project"}, + np, + ) + assert.NoError(t, err) +} + +func buildNetworkPolicyTestObjects() (*chev2.CheCluster, client.Object, client.Object) { + cheCluster := &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eclipse-che", + Namespace: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + NetworkPolicy: &chev2.NetworkPolicy{ + Enabled: ptr.To(true), + }, + }, + }, + } + + userNamespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-project", + Labels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + constants.KubernetesComponentLabelKey: "workspaces-namespace", + constants.WorkspaceNamespaceOwnerUidLabelKey: "some-uid", + }, + }, + } + + userProject := &projectv1.Project{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-project", + Labels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + constants.KubernetesComponentLabelKey: "workspaces-namespace", + constants.WorkspaceNamespaceOwnerUidLabelKey: "some-uid", + }, + }, + } + + return cheCluster, userNamespace, userProject +} diff --git a/controllers/usernamespace/usernamespace_controller_setup_test.go b/controllers/usernamespace/usernamespace_controller_setup_test.go new file mode 100644 index 0000000000..d278f4be11 --- /dev/null +++ b/controllers/usernamespace/usernamespace_controller_setup_test.go @@ -0,0 +1,170 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package usernamespace + +import ( + "context" + "sync" + "testing" + + "github.com/eclipse-che/che-operator/controllers/namespacecache" + k8sclient "github.com/eclipse-che/che-operator/pkg/common/k8s-client" + "k8s.io/utils/ptr" + + "github.com/eclipse-che/che-operator/pkg/common/test" + + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/common/infrastructure" + "github.com/eclipse-che/che-operator/pkg/deploy/tls" + projectv1 "github.com/openshift/api/project/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func setupCheCluster(t *testing.T, ctx context.Context, cl client.Client, cheNamespaceName string, cheName string) { + var cheNamespace metav1.Object + if infrastructure.IsOpenShift() { + cheNamespace = &projectv1.Project{} + } else { + cheNamespace = &corev1.Namespace{} + } + + cheNamespace.SetName(cheNamespaceName) + if err := cl.Create(ctx, cheNamespace.(client.Object)); err != nil { + t.Fatal(err) + } + + cheCluster := chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: cheName, + Namespace: cheNamespaceName, + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + NodeSelector: map[string]string{"a": "b", "c": "d"}, + Tolerations: []corev1.Toleration{ + { + Key: "a", + Operator: corev1.TolerationOpEqual, + Value: "b", + }, + { + Key: "c", + Operator: corev1.TolerationOpEqual, + Value: "d", + }, + }, + TrustedCerts: &chev2.TrustedCerts{ + GitTrustedCertsConfigMapName: "che-git-self-signed-cert", + }, + SecondsOfInactivityBeforeIdling: ptr.To(int32(1800)), + SecondsOfRunBeforeIdling: ptr.To(int32(-1)), + EditorsDownloadUrls: []chev2.EditorDownloadUrl{ + { + Editor: "che-incubator/che-idea/latest", + Url: "url_latest", + }, + { + Editor: "che-incubator/che-idea/next", + Url: "url_next", + }, + }, + }, + Networking: chev2.CheClusterSpecNetworking{ + Domain: "root-domain", + }, + }, + Status: chev2.CheClusterStatus{ + CheURL: "https://che-host", + }, + } + if err := cl.Create(ctx, &cheCluster); err != nil { + t.Fatal(err) + } + + // also create the self-signed-certificate secret to pretend we have TLS set up + cert := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.DefaultSelfSignedCertificateSecretName, + Namespace: cheNamespaceName, + }, + Data: map[string][]byte{ + "ca.crt": []byte("my certificate"), + "other.data": []byte("should not be copied to target ns"), + }, + Type: "Opaque", + Immutable: ptr.To(true), + } + if err := cl.Create(ctx, cert); err != nil { + t.Fatal(err) + } + + caCerts := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: cheNamespaceName, + }, + Data: map[string]string{ + "trusted1": "trusted cert 1", + "trusted2": "trusted cert 2", + }, + } + if err := cl.Create(ctx, caCerts); err != nil { + t.Fatal(err) + } + + gitTlsCredentials := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che-git-self-signed-cert", + Namespace: cheNamespaceName, + }, + Data: map[string]string{ + "githost": "the.host.of.git", + "ca.crt": "the public certificate of the.host.of.git", + }, + } + if err := cl.Create(ctx, gitTlsCredentials); err != nil { + t.Fatal(err) + } + +} + +func setup(infraType infrastructure.Type, objs ...client.Object) (*runtime.Scheme, client.Client, *CheUserNamespaceReconciler) { + infrastructure.InitializeForTesting(infraType) + + ctx := test.NewCtxBuilder().WithObjects(objs...).WithCheCluster(nil).Build() + + cl := ctx.ClusterAPI.Client + scheme := ctx.ClusterAPI.Scheme + + r := &CheUserNamespaceReconciler{ + client: cl, + nonCachedClient: cl, + clientWrapper: k8sclient.NewK8sClient(cl, scheme), + nonCachedClientWrapper: k8sclient.NewK8sClient(cl, scheme), + scheme: scheme, + namespaceCache: &namespacecache.NamespaceCache{ + Client: cl, + KnownNamespaces: map[string]namespacecache.NamespaceInfo{}, + Lock: sync.Mutex{}, + }, + } + + r.setDWONamespace("devworkspace-controller") + + return scheme, cl, r +} diff --git a/controllers/usernamespace/usernamespace_controller_test.go b/controllers/usernamespace/usernamespace_controller_test.go index a623ace852..2bd40cc1d0 100644 --- a/controllers/usernamespace/usernamespace_controller_test.go +++ b/controllers/usernamespace/usernamespace_controller_test.go @@ -15,16 +15,11 @@ package usernamespace import ( "context" "encoding/json" - "sync" "testing" - "github.com/eclipse-che/che-operator/controllers/namespacecache" - k8sclient "github.com/eclipse-che/che-operator/pkg/common/k8s-client" containerbuild "github.com/eclipse-che/che-operator/pkg/deploy/container-capabilities" "k8s.io/utils/ptr" - "github.com/eclipse-che/che-operator/pkg/common/test" - rbacv1 "k8s.io/api/rbac/v1" dwconstants "github.com/devfile/devworkspace-operator/pkg/constants" @@ -37,7 +32,6 @@ import ( "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/client" @@ -45,143 +39,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -func setupCheCluster(t *testing.T, ctx context.Context, cl client.Client, scheme *runtime.Scheme, cheNamespaceName string, cheName string) { - var cheNamespace metav1.Object - if infrastructure.IsOpenShift() { - cheNamespace = &projectv1.Project{} - } else { - cheNamespace = &corev1.Namespace{} - } - - cheNamespace.SetName(cheNamespaceName) - if err := cl.Create(ctx, cheNamespace.(client.Object)); err != nil { - t.Fatal(err) - } - - cheCluster := chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: cheName, - Namespace: cheNamespaceName, - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - NodeSelector: map[string]string{"a": "b", "c": "d"}, - Tolerations: []corev1.Toleration{ - { - Key: "a", - Operator: corev1.TolerationOpEqual, - Value: "b", - }, - { - Key: "c", - Operator: corev1.TolerationOpEqual, - Value: "d", - }, - }, - TrustedCerts: &chev2.TrustedCerts{ - GitTrustedCertsConfigMapName: "che-git-self-signed-cert", - }, - SecondsOfInactivityBeforeIdling: ptr.To(int32(1800)), - SecondsOfRunBeforeIdling: ptr.To(int32(-1)), - EditorsDownloadUrls: []chev2.EditorDownloadUrl{ - { - Editor: "che-incubator/che-idea/latest", - Url: "url_latest", - }, - { - Editor: "che-incubator/che-idea/next", - Url: "url_next", - }, - }, - }, - Networking: chev2.CheClusterSpecNetworking{ - Domain: "root-domain", - }, - }, - Status: chev2.CheClusterStatus{ - CheURL: "https://che-host", - }, - } - if err := cl.Create(ctx, &cheCluster); err != nil { - t.Fatal(err) - } - - // also create the self-signed-certificate secret to pretend we have TLS set up - cert := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: constants.DefaultSelfSignedCertificateSecretName, - Namespace: cheNamespaceName, - }, - Data: map[string][]byte{ - "ca.crt": []byte("my certificate"), - "other.data": []byte("should not be copied to target ns"), - }, - Type: "Opaque", - Immutable: ptr.To(true), - } - if err := cl.Create(ctx, cert); err != nil { - t.Fatal(err) - } - - caCerts := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: tls.CheMergedCABundleCertsCMName, - Namespace: cheNamespaceName, - }, - Data: map[string]string{ - "trusted1": "trusted cert 1", - "trusted2": "trusted cert 2", - }, - } - if err := cl.Create(ctx, caCerts); err != nil { - t.Fatal(err) - } - - gitTlsCredentials := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "che-git-self-signed-cert", - Namespace: cheNamespaceName, - }, - Data: map[string]string{ - "githost": "the.host.of.git", - "ca.crt": "the public certificate of the.host.of.git", - }, - } - if err := cl.Create(ctx, gitTlsCredentials); err != nil { - t.Fatal(err) - } - -} - -func setup(infraType infrastructure.Type, objs ...client.Object) (*runtime.Scheme, client.Client, *CheUserNamespaceReconciler) { - infrastructure.InitializeForTesting(infraType) - - ctx := test.NewCtxBuilder().WithObjects(objs...).WithCheCluster(nil).Build() - cl := ctx.ClusterAPI.Client - scheme := ctx.ClusterAPI.Scheme - - r := &CheUserNamespaceReconciler{ - client: cl, - nonCachedClient: cl, - clientWrapper: k8sclient.NewK8sClient(cl, scheme), - nonCachedClientWrapper: k8sclient.NewK8sClient(cl, scheme), - scheme: scheme, - namespaceCache: &namespacecache.NamespaceCache{ - Client: cl, - KnownNamespaces: map[string]namespacecache.NamespaceInfo{}, - Lock: sync.Mutex{}, - }, - } - - return scheme, cl, r -} - func TestSkipsUnlabeledNamespaces(t *testing.T) { test := func(t *testing.T, infraType infrastructure.Type, namespace metav1.Object) { ctx := context.TODO() - scheme, cl, r := setup(infraType, namespace.(client.Object)) - setupCheCluster(t, ctx, cl, scheme, "che", "che") + _, cl, r := setup(infraType, namespace.(client.Object)) + setupCheCluster(t, ctx, cl, "che", "che") if _, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: namespace.GetName()}}); err != nil { t.Fatal(err) @@ -239,8 +101,8 @@ func TestCreatesDataInNamespace(t *testing.T) { test := func(t *testing.T, infraType infrastructure.Type, namespace client.Object, objs ...client.Object) { ctx := context.TODO() allObjs := append(objs, namespace) - scheme, cl, r := setup(infraType, allObjs...) - setupCheCluster(t, ctx, cl, scheme, "eclipse-che", "che") + _, cl, r := setup(infraType, allObjs...) + setupCheCluster(t, ctx, cl, "eclipse-che", "che") res, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: namespace.GetName()}}) assert.NoError(t, err, "Reconciliation should have succeeded") diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index ff71d11434..85776d97d1 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -27863,6 +27863,31 @@ spec: description: Defines labels which will be set for an Ingress (a route for OpenShift platform). type: object + networkPolicy: + description: |- + NetworkPolicy configures NetworkPolicy resources for the Che namespace + and user workspace namespaces. For OpenShift clusters only. + When enabled, the following policies are created: + In the Che namespace: + - allow-from-same-namespace: allows ingress traffic between Che pods in the same namespace. + - allow-from-workspaces: allows ingress traffic from user workspace namespaces. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from--operator: allows ingress traffic from the operator pod to Che components. + - allow-all-egress: allows all egress traffic from Che pods. + In each user workspace namespace: + - allow-from-: allows ingress traffic from the Che namespace. + - allow-from-same-namespace: allows ingress traffic between pods in the same namespace. + - allow-from-devworkspace-operator: allows ingress traffic from the DevWorkspace operator. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-all-egress: allows all egress traffic from workspace pods. + properties: + enabled: + description: Enabled controls whether the operator creates + NetworkPolicy resources. + type: boolean + type: object tlsSecretName: description: |- The name of the secret used to set up Ingress TLS termination. diff --git a/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml index 866076f6a9..d428d77f78 100644 --- a/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml @@ -27858,6 +27858,31 @@ spec: description: Defines labels which will be set for an Ingress (a route for OpenShift platform). type: object + networkPolicy: + description: |- + NetworkPolicy configures NetworkPolicy resources for the Che namespace + and user workspace namespaces. For OpenShift clusters only. + When enabled, the following policies are created: + In the Che namespace: + - allow-from-same-namespace: allows ingress traffic between Che pods in the same namespace. + - allow-from-workspaces: allows ingress traffic from user workspace namespaces. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from--operator: allows ingress traffic from the operator pod to Che components. + - allow-all-egress: allows all egress traffic from Che pods. + In each user workspace namespace: + - allow-from-: allows ingress traffic from the Che namespace. + - allow-from-same-namespace: allows ingress traffic between pods in the same namespace. + - allow-from-devworkspace-operator: allows ingress traffic from the DevWorkspace operator. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-all-egress: allows all egress traffic from workspace pods. + properties: + enabled: + description: Enabled controls whether the operator creates + NetworkPolicy resources. + type: boolean + type: object tlsSecretName: description: |- The name of the secret used to set up Ingress TLS termination. diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index 7b726244b3..851f5b1d4c 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -27863,6 +27863,31 @@ spec: description: Defines labels which will be set for an Ingress (a route for OpenShift platform). type: object + networkPolicy: + description: |- + NetworkPolicy configures NetworkPolicy resources for the Che namespace + and user workspace namespaces. For OpenShift clusters only. + When enabled, the following policies are created: + In the Che namespace: + - allow-from-same-namespace: allows ingress traffic between Che pods in the same namespace. + - allow-from-workspaces: allows ingress traffic from user workspace namespaces. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from--operator: allows ingress traffic from the operator pod to Che components. + - allow-all-egress: allows all egress traffic from Che pods. + In each user workspace namespace: + - allow-from-: allows ingress traffic from the Che namespace. + - allow-from-same-namespace: allows ingress traffic between pods in the same namespace. + - allow-from-devworkspace-operator: allows ingress traffic from the DevWorkspace operator. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-all-egress: allows all egress traffic from workspace pods. + properties: + enabled: + description: Enabled controls whether the operator creates + NetworkPolicy resources. + type: boolean + type: object tlsSecretName: description: |- The name of the secret used to set up Ingress TLS termination. diff --git a/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml index d2cfaeb69c..90d3557d71 100644 --- a/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml @@ -27858,6 +27858,31 @@ spec: description: Defines labels which will be set for an Ingress (a route for OpenShift platform). type: object + networkPolicy: + description: |- + NetworkPolicy configures NetworkPolicy resources for the Che namespace + and user workspace namespaces. For OpenShift clusters only. + When enabled, the following policies are created: + In the Che namespace: + - allow-from-same-namespace: allows ingress traffic between Che pods in the same namespace. + - allow-from-workspaces: allows ingress traffic from user workspace namespaces. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from--operator: allows ingress traffic from the operator pod to Che components. + - allow-all-egress: allows all egress traffic from Che pods. + In each user workspace namespace: + - allow-from-: allows ingress traffic from the Che namespace. + - allow-from-same-namespace: allows ingress traffic between pods in the same namespace. + - allow-from-devworkspace-operator: allows ingress traffic from the DevWorkspace operator. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-all-egress: allows all egress traffic from workspace pods. + properties: + enabled: + description: Enabled controls whether the operator creates + NetworkPolicy resources. + type: boolean + type: object tlsSecretName: description: |- The name of the secret used to set up Ingress TLS termination. diff --git a/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml b/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml index 866076f6a9..d428d77f78 100644 --- a/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml +++ b/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml @@ -27858,6 +27858,31 @@ spec: description: Defines labels which will be set for an Ingress (a route for OpenShift platform). type: object + networkPolicy: + description: |- + NetworkPolicy configures NetworkPolicy resources for the Che namespace + and user workspace namespaces. For OpenShift clusters only. + When enabled, the following policies are created: + In the Che namespace: + - allow-from-same-namespace: allows ingress traffic between Che pods in the same namespace. + - allow-from-workspaces: allows ingress traffic from user workspace namespaces. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from--operator: allows ingress traffic from the operator pod to Che components. + - allow-all-egress: allows all egress traffic from Che pods. + In each user workspace namespace: + - allow-from-: allows ingress traffic from the Che namespace. + - allow-from-same-namespace: allows ingress traffic between pods in the same namespace. + - allow-from-devworkspace-operator: allows ingress traffic from the DevWorkspace operator. + - allow-from-openshift-monitoring: allows ingress traffic from the OpenShift monitoring namespace. + - allow-from-openshift-ingress: allows ingress traffic from the OpenShift ingress namespace. + - allow-all-egress: allows all egress traffic from workspace pods. + properties: + enabled: + description: Enabled controls whether the operator creates + NetworkPolicy resources. + type: boolean + type: object tlsSecretName: description: |- The name of the secret used to set up Ingress TLS termination. diff --git a/helmcharts/next/templates/org_v2_checluster.yaml b/helmcharts/next/templates/org_v2_checluster.yaml index 64c039e0c2..4f8a94efef 100644 --- a/helmcharts/next/templates/org_v2_checluster.yaml +++ b/helmcharts/next/templates/org_v2_checluster.yaml @@ -9,6 +9,7 @@ # Contributors: # Red Hat, Inc. - initial API and implementation # + apiVersion: org.eclipse.che/v2 kind: CheCluster metadata: diff --git a/pkg/common/chetypes/types.go b/pkg/common/chetypes/types.go index 8ccd2d461c..62d624fda6 100644 --- a/pkg/common/chetypes/types.go +++ b/pkg/common/chetypes/types.go @@ -13,6 +13,8 @@ package chetypes import ( + "context" + chev2 "github.com/eclipse-che/che-operator/api/v2" k8sclient "github.com/eclipse-che/che-operator/pkg/common/k8s-client" "k8s.io/apimachinery/pkg/runtime" @@ -27,7 +29,8 @@ type DeployContext struct { Authentication *Authentication IsSelfSignedCertificate bool CheHost string - DwoNamespace string + DWONamespace string + Context context.Context } type ClusterAPI struct { diff --git a/pkg/common/constants/constants.go b/pkg/common/constants/constants.go index 3db5587395..ca28daef01 100644 --- a/pkg/common/constants/constants.go +++ b/pkg/common/constants/constants.go @@ -180,6 +180,9 @@ const ( DefaultContainerRunSccName = "container-run" DefaultDisableContainerRunCapabilities = true + // Networking + NetworkPolicyEnabled = false + // Finalizers ContainerBuildFinalizer = "container-build.finalizers.che.eclipse.org" ContainerRunFinalizer = "che.eclipse.org/container-run" diff --git a/pkg/common/diffs/diffs.go b/pkg/common/diffs/diffs.go index 3e008b10dd..cf88bca460 100644 --- a/pkg/common/diffs/diffs.go +++ b/pkg/common/diffs/diffs.go @@ -81,6 +81,10 @@ func ConfigMap(labelKeys []string, annotationKeys []string) cmp.Options { } } +var NetworkPolicy = cmp.Options{ + cmpopts.IgnoreFields(networking.NetworkPolicy{}, "TypeMeta", "ObjectMeta"), +} + var ServiceMonitor = cmp.Options{ cmpopts.IgnoreFields(monitoringv1.ServiceMonitor{}, "TypeMeta", "ObjectMeta"), } diff --git a/pkg/common/infrastructure/cluster.go b/pkg/common/infrastructure/cluster.go index 04d99e7252..0fcba1fc20 100644 --- a/pkg/common/infrastructure/cluster.go +++ b/pkg/common/infrastructure/cluster.go @@ -47,6 +47,8 @@ var ( operatorNamespace string ) +// GetOperatorNamespace returns the namespace where the operator is running. +// The result is cached to avoid repeated filesystem reads. func GetOperatorNamespace() (string, error) { if operatorNamespace == "" { nsBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") diff --git a/pkg/common/k8s-client/k8s_client.go b/pkg/common/k8s-client/k8s_client.go index 38e43494e8..41a3ed3e5a 100644 --- a/pkg/common/k8s-client/k8s_client.go +++ b/pkg/common/k8s-client/k8s_client.go @@ -127,6 +127,18 @@ func (k K8sClientWrapper) DeleteByKeyIgnoreNotFound( return k.deleteByKeyIgnoreNotFound(ctx, key, objectMeta, opts...) } +func (k K8sClientWrapper) DeleteIgnoreNotFound( + ctx context.Context, + obj client.Object, + opts ...client.DeleteOption, +) error { + if err := k.ensureGVK(obj); err != nil { + return err + } + + return k.doDeleteIgnoreIfNotFound(ctx, obj, opts...) +} + func (k K8sClientWrapper) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) ([]runtime.Object, error) { err := k.cli.List(ctx, list, opts...) if err != nil { diff --git a/pkg/common/k8s-client/k8s_client_types.go b/pkg/common/k8s-client/k8s_client_types.go index b2252a3e55..47d0782798 100644 --- a/pkg/common/k8s-client/k8s_client_types.go +++ b/pkg/common/k8s-client/k8s_client_types.go @@ -36,6 +36,9 @@ type K8sClient interface { // Returns true if object exists otherwise returns false. // Returns nil if object is retrieved or not found otherwise returns error. GetIgnoreNotFound(ctx context.Context, key client.ObjectKey, objectMeta client.Object, opts ...client.GetOption) (bool, error) + // DeleteIgnoreNotFound deletes object. + // Returns nil if object is deleted or not found otherwise returns error. + DeleteIgnoreNotFound(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error // DeleteByKeyIgnoreNotFound deletes object by key. // Returns nil if object is deleted or not found otherwise returns error. DeleteByKeyIgnoreNotFound(ctx context.Context, key client.ObjectKey, objectMeta client.Object, opts ...client.DeleteOption) error diff --git a/pkg/common/test/deploy_context.go b/pkg/common/test/deploy_context.go index 0832e3ac42..4ba7ad5fd1 100644 --- a/pkg/common/test/deploy_context.go +++ b/pkg/common/test/deploy_context.go @@ -13,6 +13,7 @@ package test import ( + "context" "strings" chev2 "github.com/eclipse-che/che-operator/api/v2" @@ -76,7 +77,8 @@ func (f *DeployContextBuild) Build() *chetypes.DeployContext { }, Proxy: &chetypes.Proxy{}, Authentication: buildAuthentication(f.cheCluster), - DwoNamespace: "devworkspace-controller", + DWONamespace: "devworkspace-controller", + Context: context.Background(), } if f.cheCluster != nil { diff --git a/pkg/common/test/test-client/test_client.go b/pkg/common/test/test-client/test_client.go index bef4f3dc28..2e99d6ed62 100644 --- a/pkg/common/test/test-client/test_client.go +++ b/pkg/common/test/test-client/test_client.go @@ -111,6 +111,7 @@ func getScheme() *runtime.Scheme { scheme.AddKnownTypes(appsv1.SchemeGroupVersion, &appsv1.Deployment{}, &appsv1.DeploymentList{}) scheme.AddKnownTypes(chev2.GroupVersion, &chev2.CheCluster{}, &chev2.CheClusterList{}) scheme.AddKnownTypes(networkingv1.SchemeGroupVersion, &networkingv1.Ingress{}, &networkingv1.IngressList{}) + scheme.AddKnownTypes(networkingv1.SchemeGroupVersion, &networkingv1.NetworkPolicy{}, &networkingv1.NetworkPolicyList{}) scheme.AddKnownTypes(batchv1.SchemeGroupVersion, &batchv1.Job{}, &batchv1.JobList{}) scheme.AddKnownTypes(projectv1.GroupVersion, &projectv1.Project{}, &projectv1.ProjectList{}) scheme.AddKnownTypes(monitoringv1.SchemeGroupVersion, &monitoringv1.ServiceMonitor{}, &monitoringv1.ServiceMonitorList{}) diff --git a/pkg/deploy/container-capabilities/container_capabilities.go b/pkg/deploy/container-capabilities/container_capabilities.go index 227534c3ba..5874fdcf37 100644 --- a/pkg/deploy/container-capabilities/container_capabilities.go +++ b/pkg/deploy/container-capabilities/container_capabilities.go @@ -124,7 +124,7 @@ func (r *ContainerCapabilitiesReconciler) sync(ctx *chetypes.DeployContext, cc C if err := ctx.ClusterAPI.ClientWrapper.Sync( context.TODO(), r.getDWClusterRoleBinding( - ctx.DwoNamespace, + ctx.DWONamespace, cc.getDWOClusterRoleName(), cc.getDWOClusterRoleBindingName(), ), @@ -148,7 +148,7 @@ func (r *ContainerCapabilitiesReconciler) sync(ctx *chetypes.DeployContext, cc C scc := &securityv1.SecurityContextConstraints{} if exists, err := ctx.ClusterAPI.NonCachingClientWrapper.GetIgnoreNotFound(context.TODO(), sccKey, scc); exists { - if deploy.IsPartOfEclipseCheResourceAndManagedByOperator(scc.Labels) { + if deploy.IsOperatorManagedComponent(scc.Labels, defaults.GetCheFlavor()) { // SCC exists and created by operator (custom SCC won't be updated). // Remove priority, see details https://issues.redhat.com/browse/CRW-389 scc.Priority = nil diff --git a/pkg/deploy/container-capabilities/container_capabilities_test.go b/pkg/deploy/container-capabilities/container_capabilities_test.go index 95aa228f84..3eb45849d7 100644 --- a/pkg/deploy/container-capabilities/container_capabilities_test.go +++ b/pkg/deploy/container-capabilities/container_capabilities_test.go @@ -202,7 +202,7 @@ func TestShouldNotSyncSCCIfAlreadyExists(t *testing.T) { } ctx := test.NewCtxBuilder().WithObjects(dwPod, sccBuild, sccRun).Build() - ctx.DwoNamespace = "devworkspace-controller" + ctx.DWONamespace = "devworkspace-controller" ctx.CheCluster.Spec.DevEnvironments.DisableContainerBuildCapabilities = ptr.To(false) ctx.CheCluster.Spec.DevEnvironments.ContainerBuildConfiguration = &chev2.ContainerBuildConfiguration{OpenShiftSecurityContextConstraint: "scc-build"} diff --git a/pkg/deploy/dashboard/deployment_dashboard.go b/pkg/deploy/dashboard/deployment_dashboard.go index 65c9b4bcc3..ccb1ea5d4e 100644 --- a/pkg/deploy/dashboard/deployment_dashboard.go +++ b/pkg/deploy/dashboard/deployment_dashboard.go @@ -95,7 +95,7 @@ func (d *DashboardReconciler) getDashboardDeploymentSpec(ctx *chetypes.DeployCon Value: ctx.CheCluster.Name}, corev1.EnvVar{ Name: "DWO_NAMESPACE", - Value: ctx.DwoNamespace, + Value: ctx.DWONamespace, }, ) diff --git a/pkg/deploy/devworkspace/dwo_namespace.go b/pkg/deploy/devworkspace/dwo_namespace.go index fa1aec959c..5ac0d1df62 100644 --- a/pkg/deploy/devworkspace/dwo_namespace.go +++ b/pkg/deploy/devworkspace/dwo_namespace.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2025 Red Hat, Inc. +// Copyright (c) 2019-2026 Red Hat, Inc. // This program and the accompanying materials are made // available under the terms of the Eclipse Public License 2.0 // which is available at https://www.eclipse.org/legal/epl-2.0/ @@ -16,40 +16,14 @@ import ( "context" "fmt" - "github.com/eclipse-che/che-operator/pkg/common/chetypes" "github.com/eclipse-che/che-operator/pkg/common/constants" - "github.com/eclipse-che/che-operator/pkg/common/reconciler" + k8sclient "github.com/eclipse-che/che-operator/pkg/common/k8s-client" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -type DwoNamespaceReconciler struct { - reconciler.Reconcilable -} - -func NewDwoNamespaceReconciler() *DwoNamespaceReconciler { - return &DwoNamespaceReconciler{} -} - -func (r *DwoNamespaceReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { - dwoNamespace, err := r.getDevWorkspaceNamespace(ctx) - if err != nil { - return reconcile.Result{}, false, err - } - - ctx.DwoNamespace = dwoNamespace - return reconcile.Result{}, true, nil -} - -func (r *DwoNamespaceReconciler) Finalize(ctx *chetypes.DeployContext) bool { - return true -} - -// getDevWorkspaceNamespace returns the namespace of the DevWorkspace operator. -// It searches for the DevWorkspace Operator Pods by its labels. -func (r *DwoNamespaceReconciler) getDevWorkspaceNamespace(ctx *chetypes.DeployContext) (string, error) { +func GetDevWorkspaceOperatorNamespace(context context.Context, clientWrapper *k8sclient.K8sClientWrapper) (string, error) { selector := labels.SelectorFromSet( labels.Set{ constants.KubernetesNameLabelKey: constants.DevWorkspaceControllerName, @@ -57,21 +31,30 @@ func (r *DwoNamespaceReconciler) getDevWorkspaceNamespace(ctx *chetypes.DeployCo }, ) - items, err := ctx.ClusterAPI.NonCachingClientWrapper.List( - context.TODO(), + items, err := clientWrapper.List( + context, &corev1.PodList{}, &client.ListOptions{LabelSelector: selector}, ) if err != nil { - return "", err + return "", fmt.Errorf("failed to list DevWorkspace operator pods: %w", err) } + devWorkspaceOperatorNamespace := "" + for _, item := range items { - pod := item.(*corev1.Pod) + pod, ok := item.(*corev1.Pod) + if !ok { + continue + } + if pod.Spec.ServiceAccountName == constants.DevWorkspaceServiceAccountName { - return pod.Namespace, nil + if devWorkspaceOperatorNamespace != "" && devWorkspaceOperatorNamespace != pod.Namespace { + return "", fmt.Errorf("multiple DevWorkspace Operator pods were found across different namespaces") + } + devWorkspaceOperatorNamespace = pod.Namespace } } - return "", fmt.Errorf("DevWorkspace namespace not found") + return devWorkspaceOperatorNamespace, nil } diff --git a/pkg/deploy/labels.go b/pkg/deploy/labels.go index cdfdb85d34..665f826d3f 100644 --- a/pkg/deploy/labels.go +++ b/pkg/deploy/labels.go @@ -71,6 +71,13 @@ func GetLabelsAndAnnotations(obj client.Object) ([]string, []string) { return slices.Collect(maps.Keys(obj.GetLabels())), slices.Collect(maps.Keys(obj.GetAnnotations())) } -func IsPartOfEclipseCheResourceAndManagedByOperator(labels map[string]string) bool { - return labels[constants.KubernetesPartOfLabelKey] == constants.CheEclipseOrg && labels[constants.KubernetesManagedByLabelKey] == GetManagedByLabel() +func IsOperatorManagedComponent(labels map[string]string, component string) bool { + return labels[constants.KubernetesPartOfLabelKey] == constants.CheEclipseOrg && + labels[constants.KubernetesManagedByLabelKey] == GetManagedByLabel() && + labels[constants.KubernetesComponentLabelKey] == component +} + +func IsDevWorkspaceComponent(labels map[string]string) bool { + return labels[constants.KubernetesPartOfLabelKey] == constants.DevWorkspaceOperatorName && + labels[constants.KubernetesNameLabelKey] == constants.DevWorkspaceControllerName } diff --git a/pkg/deploy/networkpolicies/init_test.go b/pkg/deploy/networkpolicies/init_test.go new file mode 100644 index 0000000000..4cb922b22e --- /dev/null +++ b/pkg/deploy/networkpolicies/init_test.go @@ -0,0 +1,26 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package networkpolicies + +import ( + "github.com/eclipse-che/che-operator/pkg/common/infrastructure" + defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" + "github.com/eclipse-che/che-operator/pkg/common/test" +) + +func init() { + test.EnableTestMode() + + infrastructure.InitializeForTesting(infrastructure.OpenShiftV4) + defaults.InitializeForTesting("../../../config/manager/manager.yaml") +} diff --git a/pkg/deploy/networkpolicies/networkpolicies.go b/pkg/deploy/networkpolicies/networkpolicies.go new file mode 100644 index 0000000000..9c8f14e767 --- /dev/null +++ b/pkg/deploy/networkpolicies/networkpolicies.go @@ -0,0 +1,389 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package networkpolicies + +import ( + "fmt" + + "github.com/eclipse-che/che-operator/pkg/common/chetypes" + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/common/diffs" + "github.com/eclipse-che/che-operator/pkg/common/infrastructure" + k8sclient "github.com/eclipse-che/che-operator/pkg/common/k8s-client" + defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" + "github.com/eclipse-che/che-operator/pkg/common/reconciler" + "github.com/eclipse-che/che-operator/pkg/deploy" + 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" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type NetworkPoliciesReconciler struct { + reconciler.Reconcilable +} + +func NewNetworkPoliciesReconciler() *NetworkPoliciesReconciler { + return &NetworkPoliciesReconciler{} +} + +func (r *NetworkPoliciesReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { + if !ctx.CheCluster.IsNetworkPoliciesEnabled() { + err := DeleteNetworkPolicy(ctx, ctx.CheCluster.Namespace) + if err != nil { + err = fmt.Errorf("failed to delete NetworkPolicy in namespace %s: %w", ctx.CheCluster.Namespace, err) + } + + return reconcile.Result{}, err == nil, err + } + + err := SyncNetworkPolicy(ctx, ctx.CheCluster.Namespace) + if err != nil { + return reconcile.Result{}, false, fmt.Errorf("failed to sync NetworkPolicy in namespace %s: %w", ctx.CheCluster.Namespace, err) + } + + return reconcile.Result{}, true, nil +} + +func (r *NetworkPoliciesReconciler) Finalize(_ *chetypes.DeployContext) bool { + return true +} + +func GetNetworkPolicies(ctx *chetypes.DeployContext, namespace string) ([]*networkingv1.NetworkPolicy, error) { + isWorkspaceNetworkPolicies := ctx.CheCluster.Namespace != namespace + + operatorNamespace, err := infrastructure.GetOperatorNamespace() + if err != nil { + return nil, fmt.Errorf("failed to get operator namespace: %w", err) + } + + var podSelector metav1.LabelSelector + + if isWorkspaceNetworkPolicies { + podSelector = metav1.LabelSelector{} + } else { + podSelector = metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + }, + } + } + + allowFromSameNamespace := &networkingv1.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: "NetworkPolicy", + APIVersion: networkingv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-from-same-namespace", + Namespace: namespace, + Labels: deploy.GetLabels(defaults.GetCheFlavor()), + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: *podSelector.DeepCopy(), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + PodSelector: podSelector.DeepCopy(), + }, + }, + }, + }, + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + }, + } + + allowFromOpenShiftIngress := &networkingv1.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: "NetworkPolicy", + APIVersion: networkingv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-from-openshift-ingress", + Namespace: namespace, + Labels: deploy.GetLabels(defaults.GetCheFlavor()), + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: *podSelector.DeepCopy(), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "network.openshift.io/policy-group": "ingress", + }, + }, + }, + }, + }, + }, + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + }, + } + + allowFromOpenShiftMonitoring := &networkingv1.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: "NetworkPolicy", + APIVersion: networkingv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-from-openshift-monitoring", + Namespace: namespace, + Labels: deploy.GetLabels(defaults.GetCheFlavor()), + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: *podSelector.DeepCopy(), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "network.openshift.io/policy-group": "monitoring", + }, + }, + }, + }, + }, + }, + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + }, + } + + allowFromWorkspaces := &networkingv1.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: "NetworkPolicy", + APIVersion: networkingv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-from-workspaces", + Namespace: namespace, + Labels: deploy.GetLabels(defaults.GetCheFlavor()), + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: *podSelector.DeepCopy(), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesComponentLabelKey: constants.WorkspacesNamespaceComponentName, + }, + }, + }, + }, + }, + }, + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + }, + } + + allowFromChe := &networkingv1.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: "NetworkPolicy", + APIVersion: networkingv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-from-" + ctx.CheCluster.Namespace, + Namespace: namespace, + Labels: deploy.GetLabels(defaults.GetCheFlavor()), + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: *podSelector.DeepCopy(), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": ctx.CheCluster.Namespace, + }, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + }, + }, + }, + }, + }, + }, + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + }, + } + + allowFromCheOperator := &networkingv1.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: "NetworkPolicy", + APIVersion: networkingv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("allow-from-%s-operator", defaults.GetCheFlavor()), + Namespace: namespace, + Labels: deploy.GetLabels(defaults.GetCheFlavor()), + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: *podSelector.DeepCopy(), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": operatorNamespace, + }, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + }, + }, + }, + }, + }, + }, + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + }, + } + + var allowFromDevWorkspaceOperator *networkingv1.NetworkPolicy + + if ctx.DWONamespace != "" { + allowFromDevWorkspaceOperator = &networkingv1.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: "NetworkPolicy", + APIVersion: networkingv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-from-devworkspace-operator", + Namespace: namespace, + Labels: deploy.GetLabels(defaults.GetCheFlavor()), + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: *podSelector.DeepCopy(), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": ctx.DWONamespace, + }, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.DevWorkspaceOperatorName, + }, + }, + }, + }, + }, + }, + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + }, + } + } + + allowAllEgress := &networkingv1.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: "NetworkPolicy", + APIVersion: networkingv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-all-egress", + Namespace: namespace, + Labels: deploy.GetLabels(defaults.GetCheFlavor()), + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: *podSelector.DeepCopy(), + Egress: []networkingv1.NetworkPolicyEgressRule{{}}, + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeEgress}, + }, + } + + networkPolicies := []*networkingv1.NetworkPolicy{ + allowFromSameNamespace, + allowFromOpenShiftIngress, + allowFromOpenShiftMonitoring, + allowAllEgress, + } + + if isWorkspaceNetworkPolicies { + networkPolicies = append(networkPolicies, allowFromChe) + if allowFromDevWorkspaceOperator != nil { + networkPolicies = append(networkPolicies, allowFromDevWorkspaceOperator) + } + } else { + networkPolicies = append(networkPolicies, allowFromWorkspaces, allowFromCheOperator) + } + + return networkPolicies, nil +} + +func SyncNetworkPolicy(ctx *chetypes.DeployContext, namespace string) error { + networkPolicies, err := GetNetworkPolicies(ctx, namespace) + if err != nil { + return fmt.Errorf("failed to get NetworkPolicy in namespace %s: %w", namespace, err) + } + + for _, networkPolicy := range networkPolicies { + if ctx.CheCluster.Namespace == namespace { + if err = controllerutil.SetControllerReference(ctx.CheCluster, networkPolicy, ctx.ClusterAPI.Scheme); err != nil { + return err + } + } + + if err := ctx.ClusterAPI.ClientWrapper.Sync( + ctx.Context, + networkPolicy, + &k8sclient.SyncOptions{DiffOpts: diffs.NetworkPolicy}, + ); err != nil { + return fmt.Errorf("failed to sync NetworkPolicy %s/%s: %w", networkPolicy.Namespace, networkPolicy.Name, err) + } + } + + return nil +} + +func DeleteNetworkPolicy(ctx *chetypes.DeployContext, namespace string) error { + items, err := ctx.ClusterAPI.ClientWrapper.List( + ctx.Context, + &networkingv1.NetworkPolicyList{}, + &client.ListOptions{ + Namespace: namespace, + LabelSelector: labels.SelectorFromSet(deploy.GetLabels(defaults.GetCheFlavor())), + }, + ) + if err != nil { + return fmt.Errorf("failed to list NetworkPolicy in namespace %s: %w", namespace, err) + } + + for _, item := range items { + networkPolicy, ok := item.(*networkingv1.NetworkPolicy) + if !ok { + continue + } + + err = ctx.ClusterAPI.ClientWrapper.DeleteIgnoreNotFound(ctx.Context, networkPolicy) + if err != nil { + return fmt.Errorf("failed to delete NetworkPolicy %s/%s: %w", networkPolicy.GetNamespace(), networkPolicy.GetName(), err) + } + } + + return nil +} diff --git a/pkg/deploy/networkpolicies/networkpolicies_test.go b/pkg/deploy/networkpolicies/networkpolicies_test.go new file mode 100644 index 0000000000..929dd77041 --- /dev/null +++ b/pkg/deploy/networkpolicies/networkpolicies_test.go @@ -0,0 +1,513 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package networkpolicies + +import ( + "context" + "slices" + "testing" + + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/chetypes" + "github.com/eclipse-che/che-operator/pkg/common/constants" + defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/stretchr/testify/assert" + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" +) + +var cheClusterPolicyNames = []string{ + "allow-from-same-namespace", + "allow-from-openshift-ingress", + "allow-from-openshift-monitoring", + "allow-from-workspaces", + "allow-from-che-operator", + "allow-all-egress", +} + +var workspacePolicyNames = []string{ + "allow-from-same-namespace", + "allow-from-openshift-ingress", + "allow-from-openshift-monitoring", + "allow-from-eclipse-che", + "allow-from-devworkspace-operator", + "allow-all-egress", +} + +func TestReconcileCreatesNetworkPoliciesWhenEnabled(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + ctx.CheCluster.Spec.Networking.NetworkPolicy = &chev2.NetworkPolicy{Enabled: ptr.To(true)} + + reconciler := NewNetworkPoliciesReconciler() + + test.EnsureReconcile(t, ctx, reconciler.Reconcile) + + for _, name := range cheClusterPolicyNames { + exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound( + context.TODO(), + types.NamespacedName{Name: name, Namespace: ctx.CheCluster.Namespace}, + &networkingv1.NetworkPolicy{}, + ) + + assert.NoError(t, err) + assert.True(t, exists) + } +} + +func TestReconcileDeletesNetworkPoliciesWhenDisabled(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + ctx.CheCluster.Spec.Networking.NetworkPolicy = &chev2.NetworkPolicy{Enabled: ptr.To(true)} + + reconciler := NewNetworkPoliciesReconciler() + + test.EnsureReconcile(t, ctx, reconciler.Reconcile) + + ctx.CheCluster.Spec.Networking.NetworkPolicy.Enabled = ptr.To(false) + + test.EnsureReconcile(t, ctx, reconciler.Reconcile) + + for _, name := range cheClusterPolicyNames { + exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound( + context.TODO(), + types.NamespacedName{Name: name, Namespace: ctx.CheCluster.Namespace}, + &networkingv1.NetworkPolicy{}, + ) + + assert.NoError(t, err) + assert.False(t, exists) + } +} + +func TestReconcileDeletesOnlyLabeledPolicies(t *testing.T) { + unownedPolicy := &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unowned-policy", + Namespace: "eclipse-che", + }, + } + + ctx := test.NewCtxBuilder().WithObjects(unownedPolicy).Build() + ctx.CheCluster.Spec.Networking.NetworkPolicy = &chev2.NetworkPolicy{Enabled: ptr.To(true)} + + reconciler := NewNetworkPoliciesReconciler() + + test.EnsureReconcile(t, ctx, reconciler.Reconcile) + + ctx.CheCluster.Spec.Networking.NetworkPolicy.Enabled = ptr.To(false) + + test.EnsureReconcile(t, ctx, reconciler.Reconcile) + + exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound( + context.TODO(), + types.NamespacedName{Name: "unowned-policy", Namespace: ctx.CheCluster.Namespace}, + &networkingv1.NetworkPolicy{}, + ) + + assert.NoError(t, err) + assert.True(t, exists) +} + +func TestReconcileIdempotent(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + ctx.CheCluster.Spec.Networking.NetworkPolicy = &chev2.NetworkPolicy{Enabled: ptr.To(true)} + + reconciler := NewNetworkPoliciesReconciler() + + test.EnsureReconcile(t, ctx, reconciler.Reconcile) + test.EnsureReconcile(t, ctx, reconciler.Reconcile) + + for _, name := range cheClusterPolicyNames { + exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound( + context.TODO(), + types.NamespacedName{Name: name, Namespace: ctx.CheCluster.Namespace}, + &networkingv1.NetworkPolicy{}, + ) + + assert.NoError(t, err) + assert.True(t, exists) + } +} + +func TestGetNetworkPoliciesPodSelectorForCheClusterNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policies, err := GetNetworkPolicies(ctx, ctx.CheCluster.Namespace) + assert.NoError(t, err) + + expectedSelector := metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + }, + } + + for _, p := range policies { + assert.Equal(t, expectedSelector, p.Spec.PodSelector) + } +} + +func TestGetNetworkPoliciesPodSelectorForWorkspaceNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policies, err := GetNetworkPolicies(ctx, "user-ns") + assert.NoError(t, err) + + expectedSelector := metav1.LabelSelector{} + + for _, p := range policies { + assert.Equal(t, expectedSelector, p.Spec.PodSelector) + } +} + +func TestGetNetworkPoliciesLabelsForCheClusterNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policies, err := GetNetworkPolicies(ctx, ctx.CheCluster.Namespace) + assert.NoError(t, err) + + expectedLabels := deploy.GetLabels(defaults.GetCheFlavor()) + + for _, p := range policies { + assert.Equal(t, expectedLabels, p.Labels) + } +} + +func TestGetNetworkPoliciesLabelsForWorkspaceNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policies, err := GetNetworkPolicies(ctx, "user-ns") + assert.NoError(t, err) + + expectedLabels := deploy.GetLabels(defaults.GetCheFlavor()) + + for _, p := range policies { + assert.Equal(t, expectedLabels, p.Labels) + } +} + +func TestGetNetworkPoliciesNamespaceForCheClusterNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policies, err := GetNetworkPolicies(ctx, ctx.CheCluster.Namespace) + assert.NoError(t, err) + + for _, p := range policies { + assert.Equal(t, ctx.CheCluster.Namespace, p.Namespace) + } +} + +func TestGetNetworkPoliciesNamespaceForWorkspaceNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policies, err := GetNetworkPolicies(ctx, "user-ns") + assert.NoError(t, err) + + for _, p := range policies { + assert.Equal(t, "user-ns", p.Namespace) + } +} + +func TestAllowFromSameNamespaceSpecForCheClusterNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, ctx.CheCluster.Namespace, "allow-from-same-namespace") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, policy.Spec.PolicyTypes) + assert.Equal(t, []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + {PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + }, + }}, + }, + }, + }, policy.Spec.Ingress) +} + +func TestAllowFromSameNamespaceSpecForWorkspaceNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, "user-ns", "allow-from-same-namespace") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, policy.Spec.PolicyTypes) + assert.Equal(t, []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + {PodSelector: &metav1.LabelSelector{}}, + }, + }, + }, policy.Spec.Ingress) +} + +func TestAllowFromOpenShiftIngressSpecForCheClusterNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, ctx.CheCluster.Namespace, "allow-from-openshift-ingress") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, policy.Spec.PolicyTypes) + assert.Equal(t, []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "network.openshift.io/policy-group": "ingress", + }, + }, + }, + }, + }, + }, policy.Spec.Ingress) +} + +func TestAllowFromOpenShiftIngressSpecForWorkspaceNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, "user-ns", "allow-from-openshift-ingress") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, policy.Spec.PolicyTypes) + assert.Equal(t, []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "network.openshift.io/policy-group": "ingress", + }, + }, + }, + }, + }, + }, policy.Spec.Ingress) +} + +func TestAllowFromOpenShiftMonitoringSpecForCheClusterNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, ctx.CheCluster.Namespace, "allow-from-openshift-monitoring") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, policy.Spec.PolicyTypes) + assert.Equal(t, []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "network.openshift.io/policy-group": "monitoring", + }, + }, + }, + }, + }, + }, policy.Spec.Ingress) +} + +func TestAllowFromOpenShiftMonitoringSpecForWorkspaceNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, "user-ns", "allow-from-openshift-monitoring") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, policy.Spec.PolicyTypes) + assert.Equal(t, []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "network.openshift.io/policy-group": "monitoring", + }, + }, + }, + }, + }, + }, policy.Spec.Ingress) +} + +func TestAllowFromWorkspacesSpec(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, ctx.CheCluster.Namespace, "allow-from-workspaces") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, policy.Spec.PolicyTypes) + assert.Equal(t, []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesComponentLabelKey: constants.WorkspacesNamespaceComponentName, + }, + }, + }, + }, + }, + }, policy.Spec.Ingress) +} + +func TestAllowFromCheOperatorSpec(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, ctx.CheCluster.Namespace, "allow-from-che-operator") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, policy.Spec.PolicyTypes) + assert.Equal(t, []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": "openshift-operators", + }, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + }, + }, + }, + }, + }, + }, policy.Spec.Ingress) +} + +func TestAllowFromCheNamespaceSpec(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, "user-ns", "allow-from-eclipse-che") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, policy.Spec.PolicyTypes) + assert.Equal(t, []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": "eclipse-che", + }, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + }, + }, + }, + }, + }, + }, policy.Spec.Ingress) +} + +func TestAllowFromDevWorkspaceOperatorSpec(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, "user-ns", "allow-from-devworkspace-operator") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, policy.Spec.PolicyTypes) + assert.Equal(t, []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": "devworkspace-controller", + }, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.DevWorkspaceOperatorName, + }, + }, + }, + }, + }, + }, policy.Spec.Ingress) +} + +func TestAllowAllEgressSpecForCheClusterNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, ctx.CheCluster.Namespace, "allow-all-egress") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeEgress}, policy.Spec.PolicyTypes) + assert.Empty(t, policy.Spec.Ingress) + assert.Equal(t, []networkingv1.NetworkPolicyEgressRule{{}}, policy.Spec.Egress) +} + +func TestAllowAllEgressSpecForWorkspaceNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + policy := findPolicy(t, ctx, "user-ns", "allow-all-egress") + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeEgress}, policy.Spec.PolicyTypes) + assert.Empty(t, policy.Spec.Ingress) + assert.Equal(t, []networkingv1.NetworkPolicyEgressRule{{}}, policy.Spec.Egress) +} + +func TestSyncNetworkPolicySetsOwnerReferenceForCheClusterNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + err := SyncNetworkPolicy(ctx, ctx.CheCluster.Namespace) + assert.NoError(t, err) + + for _, name := range cheClusterPolicyNames { + networkPolicy := &networkingv1.NetworkPolicy{} + exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound( + context.TODO(), + types.NamespacedName{Name: name, Namespace: "eclipse-che"}, + networkPolicy, + ) + + assert.NoError(t, err) + assert.True(t, exists) + assert.NotEmpty(t, networkPolicy.OwnerReferences) + assert.Equal(t, ctx.CheCluster.Name, networkPolicy.OwnerReferences[0].Name) + } +} + +func TestSyncNetworkPolicyNoOwnerReferenceForWorkspaceNamespace(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + err := SyncNetworkPolicy(ctx, "user-ns") + assert.NoError(t, err) + + for _, name := range workspacePolicyNames { + networkPolicy := &networkingv1.NetworkPolicy{} + exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound( + context.TODO(), + types.NamespacedName{Name: name, Namespace: "user-ns"}, + networkPolicy, + ) + + assert.NoError(t, err) + assert.True(t, exists) + assert.Empty(t, networkPolicy.OwnerReferences) + } +} + +func findPolicy(t *testing.T, ctx *chetypes.DeployContext, namespace string, name string) *networkingv1.NetworkPolicy { + t.Helper() + + policies, err := GetNetworkPolicies(ctx, namespace) + assert.NoError(t, err) + + idx := slices.IndexFunc(policies, func(item *networkingv1.NetworkPolicy) bool { return item.Name == name }) + if idx == -1 { + t.Fatalf("policy %s not found in namespace %s", name, namespace) + } + + return policies[idx] +} diff --git a/pkg/deploy/openvsx/openvsx_secret.go b/pkg/deploy/openvsx/openvsx_secret.go index d3c623caf0..45f0f488d1 100644 --- a/pkg/deploy/openvsx/openvsx_secret.go +++ b/pkg/deploy/openvsx/openvsx_secret.go @@ -108,7 +108,7 @@ func HasCustomCredentialsSecret(ctx *chetypes.DeployContext) (bool, error) { return false, fmt.Errorf("failed to get secret: %w", err) } if exists { - return !deploy.IsPartOfEclipseCheResourceAndManagedByOperator(secret.Labels), nil + return !deploy.IsOperatorManagedComponent(secret.Labels, constants.OpenVSXDatabaseComponentName), nil } return false, nil