From ac40d355e65c49e0a17854e0f73a515fa784f5ba Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Fri, 18 Sep 2026 12:25:42 +0200 Subject: [PATCH 1/7] chore: networkpolicy provisioning Signed-off-by: Anatolii Bazko --- .../devworkspaceoperatorconfig_types.go | 41 ++ .../v1alpha1/zz_generated.deepcopy.go | 40 ++ .../devworkspacerouting_controller.go | 1 - .../workspace/devworkspace_controller.go | 11 + ...evfile.io_devworkspaceoperatorconfigs.yaml | 396 +++++++++++++++++ deploy/deployment/kubernetes/combined.yaml | 410 ++++++++++++++++++ ...r.devfile.io.CustomResourceDefinition.yaml | 410 ++++++++++++++++++ deploy/deployment/openshift/combined.yaml | 410 ++++++++++++++++++ ...r.devfile.io.CustomResourceDefinition.yaml | 410 ++++++++++++++++++ ...evfile.io_devworkspaceoperatorconfigs.yaml | 410 ++++++++++++++++++ docs/dwo-configuration.md | 41 ++ pkg/cache/cache.go | 3 + pkg/common/naming.go | 4 + pkg/config/common_test.go | 1 + pkg/config/defaults.go | 64 +++ pkg/config/sync.go | 22 +- pkg/config/sync_test.go | 10 + pkg/constants/constants.go | 4 + pkg/provision/sync/diff.go | 1 + pkg/provision/sync/diffopts.go | 4 + pkg/provision/sync/sync.go | 2 + .../workspace/networkpolicy/networkpolicy.go | 153 +++++++ .../networkpolicy/networkpolicy_test.go | 389 +++++++++++++++++ test/e2e/pkg/tests/networkpolicy_tests.go | 233 ++++++++++ .../network-policy-test-workspace.yaml | 15 + 25 files changed, 3483 insertions(+), 2 deletions(-) create mode 100644 pkg/provision/workspace/networkpolicy/networkpolicy.go create mode 100644 pkg/provision/workspace/networkpolicy/networkpolicy_test.go create mode 100644 test/e2e/pkg/tests/networkpolicy_tests.go create mode 100644 test/resources/network-policy-test-workspace.yaml diff --git a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go index 680a2abd0..5c81d49ef 100644 --- a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go +++ b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go @@ -21,6 +21,7 @@ import ( dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -285,6 +286,9 @@ type WorkspaceConfig struct { // Overrides defines configuration options for `container-overrides` and // `pod-overrides` DevWorkspace attributes. Overrides *OverrideConfig `json:"overrides,omitempty"` + // NetworkPolicy defines configuration options for the NetworkPolicy provisioned + // for each DevWorkspace. + NetworkPolicy *NetworkPolicyConfig `json:"networkPolicy,omitempty"` } type WebhookConfig struct { @@ -320,6 +324,43 @@ type PersistentHomeConfig struct { DisableInitContainer *bool `json:"disableInitContainer,omitempty"` } +// NetworkPolicyConfig defines the NetworkPolicy the DevWorkspace Operator provisions for +// DevWorkspaces. One NetworkPolicy is created per DevWorkspace and applies to that +// workspace's pods only. The policy is owned by its DevWorkspace and is removed along +// with it. +// +// The name, labels, podSelector and policyTypes of the NetworkPolicy are controlled by +// the DevWorkspace Operator; only the ingress and egress rules are configurable. +type NetworkPolicyConfig struct { + // Enabled determines whether a NetworkPolicy is provisioned for each DevWorkspace. + // Disabled by default. Changing this field does not immediately affect existing + // DevWorkspaces: changing the DevWorkspaceOperatorConfig does not enqueue the + // DevWorkspaces it affects, so the new value is applied to a DevWorkspace the next + // time that DevWorkspace is reconciled for any reason. Restarting a workspace is not + // required. Both enabling and disabling apply to running and stopped DevWorkspaces + // alike. + Enabled *bool `json:"enabled,omitempty"` + // Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not + // specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, + // the defaults allow traffic from the operator's own namespace and from the OpenShift + // monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, + // the default allows all ingress traffic, since the namespace of the cluster's ingress + // controller is not known to the operator; administrators are expected to replace this + // with rules appropriate to their cluster. + // If this field is specified as an empty list, all ingress traffic to DevWorkspace pods + // is denied. If this field is specified as a non-empty list, exactly those rules apply + // and the default rules no longer apply. + // +kubebuilder:validation:Optional + Ingress []networkingv1.NetworkPolicyIngressRule `json:"ingress,omitempty"` + // Egress defines the egress rules applied to DevWorkspace pods. If this field is not + // specified, the default egress rule of the DevWorkspace Operator applies, which allows + // all egress traffic. If this field is specified as an empty list, all egress traffic + // from DevWorkspace pods is denied. If this field is specified as a non-empty list, + // exactly those rules apply and the default rule no longer applies. + // +kubebuilder:validation:Optional + Egress []networkingv1.NetworkPolicyEgressRule `json:"egress,omitempty"` +} + type Proxy struct { // HttpProxy is the URL of the proxy for HTTP requests, in the format http://USERNAME:PASSWORD@SERVER:PORT/. To ignore // automatically detected proxy settings for the cluster, set this field to an empty string ("") diff --git a/apis/controller/v1alpha1/zz_generated.deepcopy.go b/apis/controller/v1alpha1/zz_generated.deepcopy.go index 0c3e6e4f9..96a3b84d4 100644 --- a/apis/controller/v1alpha1/zz_generated.deepcopy.go +++ b/apis/controller/v1alpha1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ package v1alpha1 import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" v1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -439,6 +440,40 @@ func (in *KeyNotFoundError) DeepCopy() *KeyNotFoundError { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkPolicyConfig) DeepCopyInto(out *NetworkPolicyConfig) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = make([]networkingv1.NetworkPolicyIngressRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Egress != nil { + in, out := &in.Egress, &out.Egress + *out = make([]networkingv1.NetworkPolicyEgressRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkPolicyConfig. +func (in *NetworkPolicyConfig) DeepCopy() *NetworkPolicyConfig { + if in == nil { + return nil + } + out := new(NetworkPolicyConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OperatorConfiguration) DeepCopyInto(out *OperatorConfiguration) { *out = *in @@ -969,6 +1004,11 @@ func (in *WorkspaceConfig) DeepCopyInto(out *WorkspaceConfig) { *out = new(OverrideConfig) (*in).DeepCopyInto(*out) } + if in.NetworkPolicy != nil { + in, out := &in.NetworkPolicy, &out.NetworkPolicy + *out = new(NetworkPolicyConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceConfig. diff --git a/controllers/controller/devworkspacerouting/devworkspacerouting_controller.go b/controllers/controller/devworkspacerouting/devworkspacerouting_controller.go index c95a4842a..1a4bbfd42 100644 --- a/controllers/controller/devworkspacerouting/devworkspacerouting_controller.go +++ b/controllers/controller/devworkspacerouting/devworkspacerouting_controller.go @@ -66,7 +66,6 @@ type DevWorkspaceRoutingReconciler struct { // +kubebuilder:rbac:groups=controller.devfile.io,resources=devworkspaceroutings/status,verbs=get;update;patch // +kubebuilder:rbac:groups="",resources=services,verbs=* // +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=* -// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=create;delete;update;patch;get;list;watch // +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=* // +kubebuidler:rbac:groups=route.openshift.io,resources=routes/status,verbs=get,list,watch // +kubebuilder:rbac:groups=route.openshift.io,resources=routes/custom-host,verbs=create diff --git a/controllers/workspace/devworkspace_controller.go b/controllers/workspace/devworkspace_controller.go index 37cc6e20d..9f731f1a0 100644 --- a/controllers/workspace/devworkspace_controller.go +++ b/controllers/workspace/devworkspace_controller.go @@ -50,12 +50,14 @@ import ( "github.com/devfile/devworkspace-operator/pkg/provision/storage" "github.com/devfile/devworkspace-operator/pkg/provision/sync" wsprovision "github.com/devfile/devworkspace-operator/pkg/provision/workspace" + "github.com/devfile/devworkspace-operator/pkg/provision/workspace/networkpolicy" "github.com/devfile/devworkspace-operator/pkg/provision/workspace/rbac" "github.com/go-logr/logr" "github.com/google/uuid" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -91,6 +93,7 @@ type DevWorkspaceReconciler struct { // +kubebuilder:rbac:groups="",resources=pods;serviceaccounts;secrets;configmaps;persistentvolumeclaims,verbs=* // +kubebuilder:rbac:groups="",resources=namespaces;events,verbs=get;list;watch // +kubebuilder:rbac:groups="batch",resources=jobs,verbs=get;create;list;watch;update;patch;delete +// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=create;delete;update;patch;get;list;watch // +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=mutatingwebhookconfigurations;validatingwebhookconfigurations,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=authorization.k8s.io,resources=subjectaccessreviews;localsubjectaccessreviews,verbs=create // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings,verbs=get;list;watch;create;update @@ -165,6 +168,13 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request return reconcile.Result{Requeue: true}, err } + // Sync the NetworkPolicy early, so that it follows the operator configuration for every + // workspace and not just the starting ones, and exists before any workspace pod does. + err = networkpolicy.SyncNetworkPolicy(workspace, clusterAPI) + if shouldReturn, reconcileResult, reconcileErr := r.checkDWError(workspace, err, "Error provisioning network policy", metrics.ReasonInfrastructureFailure, reqLogger, &reconcileStatus); shouldReturn { + return reconcileResult, reconcileErr + } + // Stop failed workspaces if workspace.Status.Phase == devworkspacePhaseFailing && workspace.Spec.Started { // If debug annotation is present, leave the deployment in place to let users @@ -818,6 +828,7 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&corev1.ConfigMap{}). Owns(&corev1.Secret{}). Owns(&corev1.ServiceAccount{}). + Owns(&networkingv1.NetworkPolicy{}). Watches(&corev1.Pod{}, handler.EnqueueRequestsFromMapFunc(dwRelatedPodsHandler)). Watches(&corev1.PersistentVolumeClaim{}, handler.EnqueueRequestsFromMapFunc(r.dwPVCHandler)). Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher). diff --git a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml index 5637227be..a7418782b 100644 --- a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -4010,6 +4010,402 @@ spec: - name type: object type: array + networkPolicy: + description: |- + NetworkPolicy defines configuration options for the NetworkPolicy provisioned + for each DevWorkspace. + properties: + egress: + description: |- + Egress defines the egress rules applied to DevWorkspace pods. If this field is not + specified, the default egress rule of the DevWorkspace Operator applies, which allows + all egress traffic. If this field is specified as an empty list, all egress traffic + from DevWorkspace pods is denied. If this field is specified as a non-empty list, + exactly those rules apply and the default rule no longer applies. + items: + description: |- + NetworkPolicyEgressRule describes a particular set of traffic that is allowed out of pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and to. + This type is beta-level in 1.8 + properties: + ports: + description: |- + ports is a list of destination ports for outgoing traffic. + Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + to: + description: |- + to is a list of destinations for outgoing traffic of pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all destinations (traffic not restricted by + destination). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the to list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + enabled: + description: |- + Enabled determines whether a NetworkPolicy is provisioned for each DevWorkspace. + Disabled by default. Changing this field does not immediately affect existing + DevWorkspaces: changing the DevWorkspaceOperatorConfig does not enqueue the + DevWorkspaces it affects, so the new value is applied to a DevWorkspace the next + time that DevWorkspace is reconciled for any reason. Restarting a workspace is not + required. Both enabling and disabling apply to running and stopped DevWorkspaces + alike. + type: boolean + ingress: + description: |- + Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not + specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, + the defaults allow traffic from the operator's own namespace and from the OpenShift + monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, + the default allows all ingress traffic, since the namespace of the cluster's ingress + controller is not known to the operator; administrators are expected to replace this + with rules appropriate to their cluster. + If this field is specified as an empty list, all ingress traffic to DevWorkspace pods + is denied. If this field is specified as a non-empty list, exactly those rules apply + and the default rules no longer apply. + items: + description: |- + NetworkPolicyIngressRule describes a particular set of traffic that is allowed to the pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and from. + properties: + from: + description: |- + from is a list of sources which should be able to access the pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all sources (traffic not restricted by + source). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the from list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + ports: + description: |- + ports is a list of ports which should be made accessible on the pods selected for + this rule. Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + type: object overrides: description: |- Overrides defines configuration options for `container-overrides` and diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index c3a61351b..e6b7126f3 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -4211,6 +4211,416 @@ spec: - name type: object type: array + networkPolicy: + description: |- + NetworkPolicy defines configuration options for the NetworkPolicy provisioned + for each DevWorkspace. + properties: + egress: + description: |- + Egress defines the egress rules applied to DevWorkspace pods. If this field is not + specified, the default egress rule of the DevWorkspace Operator applies, which allows + all egress traffic. If this field is specified as an empty list, all egress traffic + from DevWorkspace pods is denied. If this field is specified as a non-empty list, + exactly those rules apply and the default rule no longer applies. + items: + description: |- + NetworkPolicyEgressRule describes a particular set of traffic that is allowed out of pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and to. + This type is beta-level in 1.8 + properties: + ports: + description: |- + ports is a list of destination ports for outgoing traffic. + Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to + allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + to: + description: |- + to is a list of destinations for outgoing traffic of pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all destinations (traffic not restricted by + destination). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the to list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + enabled: + description: |- + Enabled determines whether a NetworkPolicy is provisioned for each DevWorkspace. + Disabled by default. Changing this field does not immediately affect existing + DevWorkspaces: changing the DevWorkspaceOperatorConfig does not enqueue the + DevWorkspaces it affects, so the new value is applied to a DevWorkspace the next + time that DevWorkspace is reconciled for any reason. Restarting a workspace is not + required. Both enabling and disabling apply to running and stopped DevWorkspaces + alike. + type: boolean + ingress: + description: |- + Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not + specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, + the defaults allow traffic from the operator's own namespace and from the OpenShift + monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, + the default allows all ingress traffic, since the namespace of the cluster's ingress + controller is not known to the operator; administrators are expected to replace this + with rules appropriate to their cluster. + If this field is specified as an empty list, all ingress traffic to DevWorkspace pods + is denied. If this field is specified as a non-empty list, exactly those rules apply + and the default rules no longer apply. + items: + description: |- + NetworkPolicyIngressRule describes a particular set of traffic that is allowed to the pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and from. + properties: + from: + description: |- + from is a list of sources which should be able to access the pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all sources (traffic not restricted by + source). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the from list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + ports: + description: |- + ports is a list of ports which should be made accessible on the pods selected for + this rule. Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to + allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + type: object overrides: description: |- Overrides defines configuration options for `container-overrides` and diff --git a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index 0eb484b64..8c28754fa 100644 --- a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -4211,6 +4211,416 @@ spec: - name type: object type: array + networkPolicy: + description: |- + NetworkPolicy defines configuration options for the NetworkPolicy provisioned + for each DevWorkspace. + properties: + egress: + description: |- + Egress defines the egress rules applied to DevWorkspace pods. If this field is not + specified, the default egress rule of the DevWorkspace Operator applies, which allows + all egress traffic. If this field is specified as an empty list, all egress traffic + from DevWorkspace pods is denied. If this field is specified as a non-empty list, + exactly those rules apply and the default rule no longer applies. + items: + description: |- + NetworkPolicyEgressRule describes a particular set of traffic that is allowed out of pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and to. + This type is beta-level in 1.8 + properties: + ports: + description: |- + ports is a list of destination ports for outgoing traffic. + Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to + allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + to: + description: |- + to is a list of destinations for outgoing traffic of pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all destinations (traffic not restricted by + destination). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the to list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + enabled: + description: |- + Enabled determines whether a NetworkPolicy is provisioned for each DevWorkspace. + Disabled by default. Changing this field does not immediately affect existing + DevWorkspaces: changing the DevWorkspaceOperatorConfig does not enqueue the + DevWorkspaces it affects, so the new value is applied to a DevWorkspace the next + time that DevWorkspace is reconciled for any reason. Restarting a workspace is not + required. Both enabling and disabling apply to running and stopped DevWorkspaces + alike. + type: boolean + ingress: + description: |- + Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not + specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, + the defaults allow traffic from the operator's own namespace and from the OpenShift + monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, + the default allows all ingress traffic, since the namespace of the cluster's ingress + controller is not known to the operator; administrators are expected to replace this + with rules appropriate to their cluster. + If this field is specified as an empty list, all ingress traffic to DevWorkspace pods + is denied. If this field is specified as a non-empty list, exactly those rules apply + and the default rules no longer apply. + items: + description: |- + NetworkPolicyIngressRule describes a particular set of traffic that is allowed to the pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and from. + properties: + from: + description: |- + from is a list of sources which should be able to access the pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all sources (traffic not restricted by + source). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the from list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + ports: + description: |- + ports is a list of ports which should be made accessible on the pods selected for + this rule. Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to + allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + type: object overrides: description: |- Overrides defines configuration options for `container-overrides` and diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index 2d5d8aa5f..ed7d2768d 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -4211,6 +4211,416 @@ spec: - name type: object type: array + networkPolicy: + description: |- + NetworkPolicy defines configuration options for the NetworkPolicy provisioned + for each DevWorkspace. + properties: + egress: + description: |- + Egress defines the egress rules applied to DevWorkspace pods. If this field is not + specified, the default egress rule of the DevWorkspace Operator applies, which allows + all egress traffic. If this field is specified as an empty list, all egress traffic + from DevWorkspace pods is denied. If this field is specified as a non-empty list, + exactly those rules apply and the default rule no longer applies. + items: + description: |- + NetworkPolicyEgressRule describes a particular set of traffic that is allowed out of pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and to. + This type is beta-level in 1.8 + properties: + ports: + description: |- + ports is a list of destination ports for outgoing traffic. + Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to + allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + to: + description: |- + to is a list of destinations for outgoing traffic of pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all destinations (traffic not restricted by + destination). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the to list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + enabled: + description: |- + Enabled determines whether a NetworkPolicy is provisioned for each DevWorkspace. + Disabled by default. Changing this field does not immediately affect existing + DevWorkspaces: changing the DevWorkspaceOperatorConfig does not enqueue the + DevWorkspaces it affects, so the new value is applied to a DevWorkspace the next + time that DevWorkspace is reconciled for any reason. Restarting a workspace is not + required. Both enabling and disabling apply to running and stopped DevWorkspaces + alike. + type: boolean + ingress: + description: |- + Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not + specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, + the defaults allow traffic from the operator's own namespace and from the OpenShift + monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, + the default allows all ingress traffic, since the namespace of the cluster's ingress + controller is not known to the operator; administrators are expected to replace this + with rules appropriate to their cluster. + If this field is specified as an empty list, all ingress traffic to DevWorkspace pods + is denied. If this field is specified as a non-empty list, exactly those rules apply + and the default rules no longer apply. + items: + description: |- + NetworkPolicyIngressRule describes a particular set of traffic that is allowed to the pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and from. + properties: + from: + description: |- + from is a list of sources which should be able to access the pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all sources (traffic not restricted by + source). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the from list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + ports: + description: |- + ports is a list of ports which should be made accessible on the pods selected for + this rule. Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to + allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + type: object overrides: description: |- Overrides defines configuration options for `container-overrides` and diff --git a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index 0eb484b64..8c28754fa 100644 --- a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -4211,6 +4211,416 @@ spec: - name type: object type: array + networkPolicy: + description: |- + NetworkPolicy defines configuration options for the NetworkPolicy provisioned + for each DevWorkspace. + properties: + egress: + description: |- + Egress defines the egress rules applied to DevWorkspace pods. If this field is not + specified, the default egress rule of the DevWorkspace Operator applies, which allows + all egress traffic. If this field is specified as an empty list, all egress traffic + from DevWorkspace pods is denied. If this field is specified as a non-empty list, + exactly those rules apply and the default rule no longer applies. + items: + description: |- + NetworkPolicyEgressRule describes a particular set of traffic that is allowed out of pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and to. + This type is beta-level in 1.8 + properties: + ports: + description: |- + ports is a list of destination ports for outgoing traffic. + Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to + allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + to: + description: |- + to is a list of destinations for outgoing traffic of pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all destinations (traffic not restricted by + destination). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the to list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + enabled: + description: |- + Enabled determines whether a NetworkPolicy is provisioned for each DevWorkspace. + Disabled by default. Changing this field does not immediately affect existing + DevWorkspaces: changing the DevWorkspaceOperatorConfig does not enqueue the + DevWorkspaces it affects, so the new value is applied to a DevWorkspace the next + time that DevWorkspace is reconciled for any reason. Restarting a workspace is not + required. Both enabling and disabling apply to running and stopped DevWorkspaces + alike. + type: boolean + ingress: + description: |- + Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not + specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, + the defaults allow traffic from the operator's own namespace and from the OpenShift + monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, + the default allows all ingress traffic, since the namespace of the cluster's ingress + controller is not known to the operator; administrators are expected to replace this + with rules appropriate to their cluster. + If this field is specified as an empty list, all ingress traffic to DevWorkspace pods + is denied. If this field is specified as a non-empty list, exactly those rules apply + and the default rules no longer apply. + items: + description: |- + NetworkPolicyIngressRule describes a particular set of traffic that is allowed to the pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and from. + properties: + from: + description: |- + from is a list of sources which should be able to access the pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all sources (traffic not restricted by + source). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the from list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + ports: + description: |- + ports is a list of ports which should be made accessible on the pods selected for + this rule. Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to + allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + type: object overrides: description: |- Overrides defines configuration options for `container-overrides` and diff --git a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml index ab5b2f381..96abef38f 100644 --- a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -4209,6 +4209,416 @@ spec: - name type: object type: array + networkPolicy: + description: |- + NetworkPolicy defines configuration options for the NetworkPolicy provisioned + for each DevWorkspace. + properties: + egress: + description: |- + Egress defines the egress rules applied to DevWorkspace pods. If this field is not + specified, the default egress rule of the DevWorkspace Operator applies, which allows + all egress traffic. If this field is specified as an empty list, all egress traffic + from DevWorkspace pods is denied. If this field is specified as a non-empty list, + exactly those rules apply and the default rule no longer applies. + items: + description: |- + NetworkPolicyEgressRule describes a particular set of traffic that is allowed out of pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and to. + This type is beta-level in 1.8 + properties: + ports: + description: |- + ports is a list of destination ports for outgoing traffic. + Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to + allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + to: + description: |- + to is a list of destinations for outgoing traffic of pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all destinations (traffic not restricted by + destination). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the to list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + enabled: + description: |- + Enabled determines whether a NetworkPolicy is provisioned for each DevWorkspace. + Disabled by default. Changing this field does not immediately affect existing + DevWorkspaces: changing the DevWorkspaceOperatorConfig does not enqueue the + DevWorkspaces it affects, so the new value is applied to a DevWorkspace the next + time that DevWorkspace is reconciled for any reason. Restarting a workspace is not + required. Both enabling and disabling apply to running and stopped DevWorkspaces + alike. + type: boolean + ingress: + description: |- + Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not + specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, + the defaults allow traffic from the operator's own namespace and from the OpenShift + monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, + the default allows all ingress traffic, since the namespace of the cluster's ingress + controller is not known to the operator; administrators are expected to replace this + with rules appropriate to their cluster. + If this field is specified as an empty list, all ingress traffic to DevWorkspace pods + is denied. If this field is specified as a non-empty list, exactly those rules apply + and the default rules no longer apply. + items: + description: |- + NetworkPolicyIngressRule describes a particular set of traffic that is allowed to the pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and from. + properties: + from: + description: |- + from is a list of sources which should be able to access the pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all sources (traffic not restricted by + source). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the from list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + ports: + description: |- + ports is a list of ports which should be made accessible on the pods selected for + this rule. Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to + allow traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + type: object overrides: description: |- Overrides defines configuration options for `container-overrides` and diff --git a/docs/dwo-configuration.md b/docs/dwo-configuration.md index c20588afc..fa18301a6 100644 --- a/docs/dwo-configuration.md +++ b/docs/dwo-configuration.md @@ -231,6 +231,47 @@ config: The config above will have newly created PVCs to have its access mode set to `ReadWriteMany`. +## Configuring Workspace NetworkPolicy + +By default, DevWorkspace pods accept traffic from anywhere in the cluster. +Administrators can enable NetworkPolicy provisioning to restrict workspace network access. +When enabled, the operator creates one NetworkPolicy per DevWorkspace, +named `-networkpolicy` in the workspace's namespace. Each policy applies only +to the pods of its own workspace, selected by the `controller.devfile.io/devworkspace_id` label. + +**Rule semantics:** +- Omitting a rule field (e.g., no `ingress` key) means the operator's default rules for that direction apply. +- Setting a rule field to an empty list (e.g., `ingress: []`) denies all traffic in that direction. +- Setting `ingress` or `egress` to a non-empty list applies exactly those rules and replaces the defaults +entirely rather than appending to them. Any default rule that should be kept must be repeated in the configuration. + +**Lifecycle:** +- A workspace's policy is synced on every reconcile of that workspace, whether it is running or stopped, +and before any workspace pod is created. +- Changing this configuration does not enqueue the workspaces it affects, so a new value reaches a given +workspace on its next reconcile rather than immediately. Restarting a workspace is not required. +- The policy is owned by its DevWorkspace, so it is garbage collected when the workspace is deleted. +- Disabling the feature (setting `enabled: false`) removes the policies of both running and stopped workspaces, +restoring connectivity rather than leaving stale policies in place. +- While the feature is enabled, the policy of a stopped workspace is left in place, where it governs no pods, +and is removed when the workspace is deleted. + +```yaml +apiVersion: controller.devfile.io/v1alpha1 +kind: DevWorkspaceOperatorConfig +metadata: + name: devworkspace-operator-config + namespace: $OPERATOR_INSTALL_NAMESPACE +config: + workspace: + networkPolicy: + enabled: true + ingress: + - {} + egress: + - {} +``` + ## Configuring Custom Init Containers The DevWorkspace Operator allows cluster administrators to inject custom init containers into all workspace pods via the `config.workspace.initContainers` field in the global DWOC. This feature enables use cases such as: diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 5d747da65..aa77c9bdb 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -92,6 +92,9 @@ func GetCacheFunc() (cache.NewCacheFunc, error) { &rbacv1.RoleBinding{}: { Label: rbacObjectSelector, }, + &networkingv1.NetworkPolicy{}: { + Label: devworkspaceObjectSelector, + }, } if infrastructure.IsOpenShift() { diff --git a/pkg/common/naming.go b/pkg/common/naming.go index e8f4e183c..c7f2504f6 100644 --- a/pkg/common/naming.go +++ b/pkg/common/naming.go @@ -166,6 +166,10 @@ func WorkspaceRolebindingName() string { return "devworkspace-default-rolebinding" } +func NetworkPolicyName(workspaceId string) string { + return fmt.Sprintf("%s-%s", workspaceId, "networkpolicy") +} + func WorkspaceSCCRoleName(sccName string) string { return fmt.Sprintf("devworkspace-use-%s", sccName) } diff --git a/pkg/config/common_test.go b/pkg/config/common_test.go index 7a161b074..07565f4f5 100644 --- a/pkg/config/common_test.go +++ b/pkg/config/common_test.go @@ -57,6 +57,7 @@ func setupForTest(t *testing.T) { setDefaultPodSecurityContext() setDefaultContainerSecurityContext() setDefaultOverrideConfig() + setDefaultNetworkPolicy() configNamespace = testNamespace originalDefaultConfig := defaultConfig.DeepCopy() t.Cleanup(func() { diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 7b299ac38..7e74bf29e 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -19,10 +19,13 @@ import ( "fmt" "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + "github.com/devfile/devworkspace-operator/pkg/constants" "github.com/devfile/devworkspace-operator/pkg/infrastructure" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/pointer" ) @@ -55,6 +58,7 @@ var defaultConfig = &v1alpha1.OperatorConfiguration{ CleanupOnStop: pointer.Bool(false), PodSecurityContext: nil, // Set per-platform in setDefaultPodSecurityContext() ContainerSecurityContext: nil, // Set per-platform in setDefaultContainerSecurityContext() + NetworkPolicy: nil, // Set per-platform in setDefaultNetworkPolicy() DefaultTemplate: nil, ProjectCloneConfig: &v1alpha1.ProjectCloneConfig{ Resources: &corev1.ResourceRequirements{ @@ -150,6 +154,21 @@ var ( }, }, } + + defaultEgressPolicyRules = []networkingv1.NetworkPolicyEgressRule{{}} + defaultKubernetesIngressPolicyRules = []networkingv1.NetworkPolicyIngressRule{{}} + defaultOpenShiftIngressPolicyRules = []networkingv1.NetworkPolicyIngressRule{ + { + From: []networkingv1.NetworkPolicyPeer{ + {NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"network.openshift.io/policy-group": "monitoring"}}}, + }, + }, + { + From: []networkingv1.NetworkPolicyPeer{ + {NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"network.openshift.io/policy-group": "ingress"}}}, + }, + }, + } ) // Necessary variables for setting pointer values @@ -158,6 +177,14 @@ var ( perWorkspaceStorageSize = resource.MustParse("10Gi") ) +// GetDefaultConfig returns a copy of the operator's default configuration. It has no +// callers inside this repository: it exists for projects that embed DWO as a dependency, +// such as che-operator, which read the defaults in order to extend them rather than +// restate them. +func GetDefaultConfig() *v1alpha1.OperatorConfiguration { + return defaultConfig.DeepCopy() +} + func setDefaultPodSecurityContext() error { if !infrastructure.IsInitialized() { return fmt.Errorf("can not set default pod security context, infrastructure not detected") @@ -193,3 +220,40 @@ func setDefaultOverrideConfig() error { } return nil } + +func setDefaultNetworkPolicy() error { + if !infrastructure.IsInitialized() { + return fmt.Errorf("can not set default network policy, infrastructure not detected") + } + operatorNamespace, err := infrastructure.GetNamespace() + if err != nil { + return err + } + + var ingressPolicyRules []networkingv1.NetworkPolicyIngressRule + if infrastructure.IsOpenShift() { + allowFromDevWorkspaceIngressPolicyRule := networkingv1.NetworkPolicyIngressRule{ + From: []networkingv1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"kubernetes.io/metadata.name": operatorNamespace}, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app.kubernetes.io/part-of": "devworkspace-operator"}, + }, + }, + }, + } + ingressPolicyRules = []networkingv1.NetworkPolicyIngressRule{allowFromDevWorkspaceIngressPolicyRule} + ingressPolicyRules = append(ingressPolicyRules, defaultOpenShiftIngressPolicyRules...) + } else { + ingressPolicyRules = defaultKubernetesIngressPolicyRules + } + + defaultConfig.Workspace.NetworkPolicy = &v1alpha1.NetworkPolicyConfig{ + Enabled: pointer.Bool(constants.DefaultNetworkPolicyEnabled), + Ingress: ingressPolicyRules, + Egress: defaultEgressPolicyRules, + } + return nil +} diff --git a/pkg/config/sync.go b/pkg/config/sync.go index 8ff002135..574654440 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -102,6 +102,7 @@ func SetGlobalConfigForTesting(testConfig *controller.OperatorConfiguration) { setDefaultPodSecurityContext() setDefaultContainerSecurityContext() setDefaultOverrideConfig() + setDefaultNetworkPolicy() internalConfig = defaultConfig.DeepCopy() mergeConfig(testConfig, internalConfig) } @@ -119,7 +120,9 @@ func SetupControllerConfig(client crclient.Client) error { if err := setDefaultOverrideConfig(); err != nil { return err } - + if err := setDefaultNetworkPolicy(); err != nil { + return err + } internalConfig = &controller.OperatorConfiguration{} namespace, err := infrastructure.GetNamespace() @@ -522,6 +525,20 @@ func mergeConfig(from, to *controller.OperatorConfiguration) { to.Workspace.Overrides.RestrictedPodOverrideFields = from.Workspace.Overrides.RestrictedPodOverrideFields } } + if from.Workspace.NetworkPolicy != nil { + if to.Workspace.NetworkPolicy == nil { + to.Workspace.NetworkPolicy = &controller.NetworkPolicyConfig{} + } + if from.Workspace.NetworkPolicy.Enabled != nil { + to.Workspace.NetworkPolicy.Enabled = from.Workspace.NetworkPolicy.Enabled + } + if from.Workspace.NetworkPolicy.Ingress != nil { + to.Workspace.NetworkPolicy.Ingress = from.Workspace.NetworkPolicy.Ingress + } + if from.Workspace.NetworkPolicy.Egress != nil { + to.Workspace.NetworkPolicy.Egress = from.Workspace.NetworkPolicy.Egress + } + } } } @@ -665,6 +682,9 @@ func GetCurrentConfigString(currConfig *controller.OperatorConfiguration) string if workspace.DeploymentStrategy != defaultConfig.Workspace.DeploymentStrategy { config = append(config, fmt.Sprintf("workspace.deploymentStrategy=%s", workspace.DeploymentStrategy)) } + if workspace.NetworkPolicy != nil && pointer.BoolDeref(workspace.NetworkPolicy.Enabled, constants.DefaultNetworkPolicyEnabled) { + config = append(config, "workspace.networkPolicy.enabled=true") + } if workspace.PVCName != defaultConfig.Workspace.PVCName { config = append(config, fmt.Sprintf("workspace.pvcName=%s", workspace.PVCName)) } diff --git a/pkg/config/sync_test.go b/pkg/config/sync_test.go index c01cada4a..0882e52bf 100644 --- a/pkg/config/sync_test.go +++ b/pkg/config/sync_test.go @@ -32,6 +32,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -110,6 +111,7 @@ func TestMergesAllFieldsFromClusterConfig(t *testing.T) { fuzzQuantity, fuzzResourceList, fuzzResourceRequirements, + fuzzIntOrString, ) for i := 0; i < 100; i++ { fuzzedConfig := &v1alpha1.OperatorConfiguration{} @@ -433,6 +435,7 @@ func TestMergeConfigLooksAtAllFields(t *testing.T) { fuzzResourceList, fuzzResourceRequirements, fuzzStringPtr, + fuzzIntOrString, ) expectedConfig := &v1alpha1.OperatorConfiguration{} actualConfig := &v1alpha1.OperatorConfiguration{} @@ -636,6 +639,13 @@ func fuzzResourceRequirements(req *corev1.ResourceRequirements, c fuzz.Continue) req.Requests = requests } +// fuzzIntOrString generates a port that can survive a round trip through the API server. +// Fuzzing the struct directly picks a random value for the Type discriminator, and any +// value other than Int or String makes marshalling fail with "impossible IntOrString.Type". +func fuzzIntOrString(port *intstr.IntOrString, c fuzz.Continue) { + *port = intstr.FromInt32(c.Int31n(65535) + 1) +} + func fuzzStringPtr(str **string, c fuzz.Continue) { randString := c.RandString() // Ensure we never assign an empty string to avoid mergeConfig skipping updates. diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 43e187e7e..b8012a066 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -114,6 +114,10 @@ const ( RbacRoleKind = "Role" // ClusterRole kind RbacClusterRoleKind = "ClusterRole" + + // DefaultNetworkPolicyEnabled defines the default value for the Enabled field, + // disabling NetworkPolicy provisioning into DevWorkspace namespaces by default. + DefaultNetworkPolicyEnabled = false ) const ( diff --git a/pkg/provision/sync/diff.go b/pkg/provision/sync/diff.go index 83e115dea..fdb3fd945 100644 --- a/pkg/provision/sync/diff.go +++ b/pkg/provision/sync/diff.go @@ -49,6 +49,7 @@ var diffFuncs = map[reflect.Type]diffFunc{ reflect.TypeOf(batchv1.Job{}): allDiffFuncs(metadataDiffFunc, jobDiffFunc), reflect.TypeOf(corev1.Service{}): allDiffFuncs(metadataDiffFunc, serviceDiffFunc), reflect.TypeOf(networkingv1.Ingress{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(ingressDiffOpts)), + reflect.TypeOf(networkingv1.NetworkPolicy{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(networkPolicyDiffOpts)), reflect.TypeOf(routev1.Route{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(routeDiffOpts)), } diff --git a/pkg/provision/sync/diffopts.go b/pkg/provision/sync/diffopts.go index 536e82f77..3e7a6e6f6 100644 --- a/pkg/provision/sync/diffopts.go +++ b/pkg/provision/sync/diffopts.go @@ -92,6 +92,10 @@ var ingressDiffOpts = cmp.Options{ cmpopts.IgnoreFields(networkingv1.HTTPIngressPath{}, "PathType"), } +var networkPolicyDiffOpts = cmp.Options{ + cmpopts.IgnoreFields(networkingv1.NetworkPolicy{}, "TypeMeta", "ObjectMeta"), +} + func getNameFromEnvFrom(source corev1.EnvFromSource) string { switch { case source.ConfigMapRef != nil: diff --git a/pkg/provision/sync/sync.go b/pkg/provision/sync/sync.go index a231e45f8..25cfd9a36 100644 --- a/pkg/provision/sync/sync.go +++ b/pkg/provision/sync/sync.go @@ -204,6 +204,8 @@ func printDiff(specObj, clusterObj crclient.Object, log logr.Logger) { diffOpts = routingDiffOpts case *networkingv1.Ingress: diffOpts = ingressDiffOpts + case *networkingv1.NetworkPolicy: + diffOpts = networkPolicyDiffOpts case *routev1.Route: diffOpts = routeDiffOpts case *corev1.Secret: diff --git a/pkg/provision/workspace/networkpolicy/networkpolicy.go b/pkg/provision/workspace/networkpolicy/networkpolicy.go new file mode 100644 index 000000000..48a09cc9a --- /dev/null +++ b/pkg/provision/workspace/networkpolicy/networkpolicy.go @@ -0,0 +1,153 @@ +// Copyright (c) 2019-2026 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package networkpolicy + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + k8sErrors "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/controller/controllerutil" + + "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + "github.com/devfile/devworkspace-operator/pkg/common" + "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/devfile/devworkspace-operator/pkg/dwerrors" + "github.com/devfile/devworkspace-operator/pkg/provision/sync" +) + +// generateNetworkPolicy builds the NetworkPolicy applied to a single DevWorkspace's pods. +// The name, labels, podSelector and policyTypes are owned by the operator; only the ingress +// and egress rules come from configuration. Both directions are always listed in policyTypes, +// so a direction whose configured rule list is empty denies all traffic in that direction. +func generateNetworkPolicy(workspace *common.DevWorkspaceWithConfig, npConfig *v1alpha1.NetworkPolicyConfig) *networkingv1.NetworkPolicy { + workspaceId := workspace.Status.DevWorkspaceId + policy := &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.NetworkPolicyName(workspaceId), + Namespace: workspace.Namespace, + Labels: map[string]string{ + constants.DevWorkspaceIDLabel: workspaceId, + constants.DevWorkspaceNameLabel: workspace.Name, + }, + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.DevWorkspaceIDLabel: workspaceId, + }, + }, + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + }, + } + + if len(npConfig.Ingress) > 0 { + ingress := make([]networkingv1.NetworkPolicyIngressRule, len(npConfig.Ingress)) + for i, rule := range npConfig.Ingress { + ruleCopy := rule.DeepCopy() + normalizePorts(ruleCopy.Ports) + ingress[i] = *ruleCopy + } + policy.Spec.Ingress = ingress + } + + if len(npConfig.Egress) > 0 { + egress := make([]networkingv1.NetworkPolicyEgressRule, len(npConfig.Egress)) + for i, rule := range npConfig.Egress { + ruleCopy := rule.DeepCopy() + normalizePorts(ruleCopy.Ports) + egress[i] = *ruleCopy + } + policy.Spec.Egress = egress + } + + return policy +} + +// normalizePorts fills in the protocol the API server would default, so that the generated +// spec compares equal to the object stored on the cluster and does not trigger an endless +// update loop. +func normalizePorts(ports []networkingv1.NetworkPolicyPort) { + for i := range ports { + if ports[i].Protocol == nil { + ports[i].Protocol = ptr.To(corev1.ProtocolTCP) + } + } +} + +func ShouldProvision(workspace *common.DevWorkspaceWithConfig) bool { + npConfig := workspace.Config.Workspace.NetworkPolicy + if npConfig == nil { + return false + } + return ptr.Deref(npConfig.Enabled, constants.DefaultNetworkPolicyEnabled) +} + +func SyncNetworkPolicy(workspace *common.DevWorkspaceWithConfig, api sync.ClusterAPI) error { + if !ShouldProvision(workspace) { + return DeleteNetworkPolicy(workspace, api) + } + return CreateNetworkPolicy(workspace, api) +} + +func CreateNetworkPolicy(workspace *common.DevWorkspaceWithConfig, api sync.ClusterAPI) error { + specPolicy := generateNetworkPolicy(workspace, workspace.Config.Workspace.NetworkPolicy) + if err := controllerutil.SetControllerReference(workspace.DevWorkspace, specPolicy, api.Scheme); err != nil { + return &dwerrors.FailError{ + Message: "failed to set owner reference on workspace network policy", + Err: err, + } + } + if _, err := sync.SyncObjectWithCluster(specPolicy, api); err != nil { + return dwerrors.WrapSyncError(err) + } + return nil +} + +// DeleteNetworkPolicy removes a DevWorkspace's NetworkPolicy if it exists. It is a no-op +// when no policy is present, so callers can invoke it unconditionally. Deleting the +// DevWorkspace itself does not require this call: the policy carries an ownerReference and +// is garbage collected with the workspace. +func DeleteNetworkPolicy(workspace *common.DevWorkspaceWithConfig, api sync.ClusterAPI) error { + name := common.NetworkPolicyName(workspace.Status.DevWorkspaceId) + policy := &networkingv1.NetworkPolicy{} + namespacedName := types.NamespacedName{ + Name: name, + Namespace: workspace.Namespace, + } + err := api.Client.Get(api.Ctx, namespacedName, policy) + switch { + case err == nil: + if err := api.Client.Delete(api.Ctx, policy); err != nil && !k8sErrors.IsNotFound(err) { + return &dwerrors.RetryError{ + Message: fmt.Sprintf("failed to delete network policy %s in namespace %s", name, workspace.Namespace), + Err: err, + } + } + api.Logger.Info("Deleted workspace network policy", "name", name, "namespace", workspace.Namespace) + return nil + case k8sErrors.IsNotFound(err): + // Already deleted + return nil + default: + return &dwerrors.RetryError{ + Message: fmt.Sprintf("failed to read network policy %s in namespace %s", name, workspace.Namespace), + Err: err, + } + } +} diff --git a/pkg/provision/workspace/networkpolicy/networkpolicy_test.go b/pkg/provision/workspace/networkpolicy/networkpolicy_test.go new file mode 100644 index 000000000..cb8c18e99 --- /dev/null +++ b/pkg/provision/workspace/networkpolicy/networkpolicy_test.go @@ -0,0 +1,389 @@ +// Copyright (c) 2019-2026 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package networkpolicy + +import ( + "context" + "fmt" + "testing" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/devworkspace-operator/pkg/config" + "github.com/go-logr/logr/testr" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + "github.com/devfile/devworkspace-operator/pkg/common" + "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/devfile/devworkspace-operator/pkg/dwerrors" + "github.com/devfile/devworkspace-operator/pkg/provision/sync" +) + +const testNamespace = "test-namespace" + +var scheme = runtime.NewScheme() + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(dw.AddToScheme(scheme)) +} + +func getTestClusterAPI(t *testing.T, initialObjects ...client.Object) sync.ClusterAPI { + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initialObjects...).Build() + return sync.ClusterAPI{ + Ctx: context.Background(), + Client: fakeClient, + Scheme: scheme, + Logger: testr.New(t), + } +} + +// getTestDevWorkspace returns a DevWorkspace whose resolved config carries the given +// network policy configuration. A nil npConfig leaves the section unset. The workspace ID +// deliberately differs from the workspace name, so that tests cannot pass by using one +// where the other is expected. +func getTestDevWorkspace(name string, npConfig *v1alpha1.NetworkPolicyConfig) *common.DevWorkspaceWithConfig { + return &common.DevWorkspaceWithConfig{ + DevWorkspace: &dw.DevWorkspace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + UID: types.UID(fmt.Sprintf("uid-%s", name)), + }, + Status: dw.DevWorkspaceStatus{ + DevWorkspaceId: fmt.Sprintf("workspace%s", name), + }, + }, + Config: config.GetConfigForTesting(&v1alpha1.OperatorConfiguration{ + Workspace: &v1alpha1.WorkspaceConfig{ + NetworkPolicy: npConfig, + }, + }), + } +} + +func enabledConfig() *v1alpha1.NetworkPolicyConfig { + return &v1alpha1.NetworkPolicyConfig{ + Enabled: ptr.To(true), + Egress: []networkingv1.NetworkPolicyEgressRule{{}}, + } +} + +// getPolicy reads the NetworkPolicy belonging to a workspace from the cluster. +func getPolicy(testdw *common.DevWorkspaceWithConfig, api sync.ClusterAPI) (*networkingv1.NetworkPolicy, error) { + actual := &networkingv1.NetworkPolicy{} + err := api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.NetworkPolicyName(testdw.Status.DevWorkspaceId), + Namespace: testdw.Namespace, + }, actual) + return actual, err +} + +func TestGeneratedPolicySelectsOnlyItsOwnWorkspacePods(t *testing.T) { + testdw := getTestDevWorkspace("test-devworkspace", enabledConfig()) + policy := generateNetworkPolicy(testdw, enabledConfig()) + assert.Equal(t, common.NetworkPolicyName(testdw.Status.DevWorkspaceId), policy.Name, "Policy should be named after the workspace it governs") + assert.Equal(t, testNamespace, policy.Namespace, "Policy should be created in the workspace namespace") + assert.Equal(t, map[string]string{ + constants.DevWorkspaceIDLabel: testdw.Status.DevWorkspaceId, + constants.DevWorkspaceNameLabel: testdw.Name, + }, policy.Labels, "Policy should carry the labels every per-workspace object carries, so that it is watched by the controller cache") + assert.Equal(t, metav1.LabelSelector{ + MatchLabels: map[string]string{ + constants.DevWorkspaceIDLabel: testdw.Status.DevWorkspaceId, + }, + }, policy.Spec.PodSelector, "Policy should select only the pods of its own workspace") +} + +func TestGeneratedPoliciesForDifferentWorkspacesDoNotCollide(t *testing.T) { + first := getTestDevWorkspace("first-devworkspace", enabledConfig()) + second := getTestDevWorkspace("second-devworkspace", enabledConfig()) + firstPolicy := generateNetworkPolicy(first, enabledConfig()) + secondPolicy := generateNetworkPolicy(second, enabledConfig()) + assert.NotEqual(t, firstPolicy.Name, secondPolicy.Name, + "Each workspace should get its own policy, otherwise two workspaces in a namespace would fight over one object") + assert.NotEqual(t, firstPolicy.Spec.PodSelector, secondPolicy.Spec.PodSelector, + "Each policy should select only its own workspace's pods") +} + +// TestUnsetIngressDeniesAllIngress covers that both directions are always listed in +// policyTypes: a direction with no configured rules is denied, not left unrestricted. +func TestUnsetIngressDeniesAllIngress(t *testing.T) { + tests := []struct { + name string + ingress []networkingv1.NetworkPolicyIngressRule + }{ + {name: "nil ingress list", ingress: nil}, + {name: "empty ingress list", ingress: []networkingv1.NetworkPolicyIngressRule{}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + npConfig := &v1alpha1.NetworkPolicyConfig{ + Enabled: ptr.To(true), + Ingress: tt.ingress, + Egress: []networkingv1.NetworkPolicyEgressRule{{}}, + } + policy := generateNetworkPolicy(getTestDevWorkspace("test-devworkspace", npConfig), npConfig) + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + policy.Spec.PolicyTypes, "Both directions should always be in policyTypes, so that a direction without rules denies all traffic") + assert.Nil(t, policy.Spec.Ingress, + "A direction without rules must serialize as nil, since an empty slice is dropped by omitempty and would never match the cluster object") + }) + } +} + +func TestBothDirectionsNilDeniesAllTraffic(t *testing.T) { + npConfig := &v1alpha1.NetworkPolicyConfig{Enabled: ptr.To(true)} + policy := generateNetworkPolicy(getTestDevWorkspace("test-devworkspace", npConfig), npConfig) + if !assert.NotNil(t, policy, "A policy should be built even when no rules are configured") { + return + } + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + policy.Spec.PolicyTypes, "With no rules configured both directions should be denied") + assert.Nil(t, policy.Spec.Ingress, "No ingress rules should be produced") + assert.Nil(t, policy.Spec.Egress, "No egress rules should be produced") +} + +func TestPortProtocolIsDefaultedToTCP(t *testing.T) { + port := intstr.FromInt(8080) + npConfig := &v1alpha1.NetworkPolicyConfig{ + Enabled: ptr.To(true), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + Ports: []networkingv1.NetworkPolicyPort{{Port: &port}}, + }, + }, + } + policy := generateNetworkPolicy(getTestDevWorkspace("test-devworkspace", npConfig), npConfig) + actual := policy.Spec.Ingress[0].Ports[0].Protocol + if !assert.NotNil(t, actual, "Protocol should be defaulted so the spec matches what the API server stores") { + return + } + assert.Equal(t, corev1.ProtocolTCP, *actual, "Omitted protocol should default to TCP") +} + +func TestExplicitPortProtocolIsPreserved(t *testing.T) { + port := intstr.FromInt(53) + udp := corev1.ProtocolUDP + npConfig := &v1alpha1.NetworkPolicyConfig{ + Enabled: ptr.To(true), + Egress: []networkingv1.NetworkPolicyEgressRule{ + { + Ports: []networkingv1.NetworkPolicyPort{{Port: &port, Protocol: &udp}}, + }, + }, + } + policy := generateNetworkPolicy(getTestDevWorkspace("test-devworkspace", npConfig), npConfig) + assert.Equal(t, corev1.ProtocolUDP, *policy.Spec.Egress[0].Ports[0].Protocol, "Explicit protocol should be preserved") +} + +func TestGenerateDoesNotMutateConfig(t *testing.T) { + port := intstr.FromInt(8080) + npConfig := &v1alpha1.NetworkPolicyConfig{ + Enabled: ptr.To(true), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + Ports: []networkingv1.NetworkPolicyPort{{Port: &port}}, + }, + }, + } + generateNetworkPolicy(getTestDevWorkspace("test-devworkspace", npConfig), npConfig) + assert.Nil(t, npConfig.Ingress[0].Ports[0].Protocol, + "Normalization must not write back into the shared resolved config") +} + +func TestSyncCreatesPolicyWhenEnabled(t *testing.T) { + testdw := getTestDevWorkspace("test-devworkspace", enabledConfig()) + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := SyncNetworkPolicy(testdw, api) + retryErr := &dwerrors.RetryError{} + if assert.Error(t, err, "Should return RetryError to indicate that the policy was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = SyncNetworkPolicy(testdw, api) + assert.NoError(t, err, "Should not return error once the policy is in sync") + + _, err = getPolicy(testdw, api) + assert.NoError(t, err, "Policy should exist on the cluster") +} + +// TestSyncOwnsPolicyByItsWorkspace covers the reason no finalizer is needed: the policy is +// garbage collected by the cluster along with the DevWorkspace that owns it. +func TestSyncOwnsPolicyByItsWorkspace(t *testing.T) { + testdw := getTestDevWorkspace("test-devworkspace", enabledConfig()) + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := SyncNetworkPolicy(testdw, api) + assert.Error(t, err, "Should return RetryError to indicate that the policy was created") + + policy, err := getPolicy(testdw, api) + if !assert.NoError(t, err, "Policy should exist on the cluster") { + return + } + if !assert.Len(t, policy.OwnerReferences, 1, "Policy should be owned by the workspace it governs, so that it is garbage collected with it") { + return + } + ownerref := policy.OwnerReferences[0] + assert.Equal(t, testdw.Name, ownerref.Name, "Policy should be owned by its own workspace") + assert.Equal(t, testdw.UID, ownerref.UID, "Policy should be owned by its own workspace") + assert.Equal(t, "DevWorkspace", ownerref.Kind, "Policy should be owned by its own workspace") + assert.True(t, ptr.Deref(ownerref.Controller, false), "Workspace should be the controller of its policy") +} + +// TestSyncTreatsApiServerDefaultedProtocolAsInSync covers the round trip normalizePorts +// exists for: the configuration omits the port protocol, while the object stored on the +// cluster carries the protocol the API server defaulted. Without normalization the two +// never compare equal and every reconcile requests another update. +func TestSyncTreatsApiServerDefaultedProtocolAsInSync(t *testing.T) { + port := intstr.FromInt(8080) + tcp := corev1.ProtocolTCP + npConfig := &v1alpha1.NetworkPolicyConfig{ + Enabled: ptr.To(true), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + Ports: []networkingv1.NetworkPolicyPort{{Port: &port}}, + }, + }, + } + + testdw := getTestDevWorkspace("test-devworkspace", npConfig) + defaultedByApiServer := generateNetworkPolicy(testdw, &v1alpha1.NetworkPolicyConfig{ + Enabled: ptr.To(true), + Ingress: []networkingv1.NetworkPolicyIngressRule{ + { + Ports: []networkingv1.NetworkPolicyPort{{Port: &port, Protocol: &tcp}}, + }, + }, + }) + + // The policy on the cluster was created by a previous sync, so it already carries the + // ownerReference; without it the sync would request an update over the missing ownerref + // rather than over the protocol under test. + err := controllerutil.SetControllerReference(testdw.DevWorkspace, defaultedByApiServer, scheme) + assert.NoError(t, err) + + api := getTestClusterAPI(t, testdw.DevWorkspace, defaultedByApiServer) + err = SyncNetworkPolicy(testdw, api) + assert.NoError(t, err, + "A policy stored with the protocol defaulted by the API server should be considered in sync, otherwise every reconcile requests an update") +} + +func TestSyncDoesNothingWhenConfigSectionIsUnset(t *testing.T) { + testdw := getTestDevWorkspace("test-devworkspace", nil) + testdw.Config.Workspace.NetworkPolicy = nil + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := SyncNetworkPolicy(testdw, api) + assert.NoError(t, err, "Should not return error when no network policy is configured") + + _, err = getPolicy(testdw, api) + assert.True(t, k8sErrors.IsNotFound(err), "No policy should be created") +} + +func TestSyncDeletesPolicyWhenDisabled(t *testing.T) { + testdw := getTestDevWorkspace("test-devworkspace", enabledConfig()) + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := SyncNetworkPolicy(testdw, api) + assert.Error(t, err, "Should return RetryError to indicate that the policy was created") + err = SyncNetworkPolicy(testdw, api) + assert.NoError(t, err, "Policy should be in sync") + + testdw.Config.Workspace.NetworkPolicy.Enabled = ptr.To(false) + err = SyncNetworkPolicy(testdw, api) + assert.NoError(t, err, "Disabling should not return an error") + + _, err = getPolicy(testdw, api) + assert.True(t, k8sErrors.IsNotFound(err), "Policy should be removed when the feature is disabled") +} + +// TestSyncLeavesOtherWorkspacesPolicyAlone guards the per-workspace scope of the delete: a +// workspace that has the feature disabled must not remove the policy governing a different +// workspace in the same namespace. +func TestSyncLeavesOtherWorkspacesPolicyAlone(t *testing.T) { + restricted := getTestDevWorkspace("restricted-devworkspace", enabledConfig()) + unrestricted := getTestDevWorkspace("unrestricted-devworkspace", nil) + unrestricted.Config.Workspace.NetworkPolicy = nil + api := getTestClusterAPI(t, restricted.DevWorkspace, unrestricted.DevWorkspace) + err := SyncNetworkPolicy(restricted, api) + assert.Error(t, err, "Should return RetryError to indicate that the policy was created") + + err = SyncNetworkPolicy(unrestricted, api) + assert.NoError(t, err, "Syncing a workspace with no policy configured should not return an error") + + _, err = getPolicy(restricted, api) + assert.NoError(t, err, "A workspace must not delete the policy that governs another workspace") +} + +func TestSyncCreatesDenyAllPolicyWhenNoRulesAreConfigured(t *testing.T) { + testdw := getTestDevWorkspace("test-devworkspace", &v1alpha1.NetworkPolicyConfig{Enabled: ptr.To(true)}) + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := SyncNetworkPolicy(testdw, api) + assert.Error(t, err, "Should return RetryError to indicate that the policy was created") + err = SyncNetworkPolicy(testdw, api) + assert.NoError(t, err, "Should not return error once the policy is in sync") + + policy, err := getPolicy(testdw, api) + if !assert.NoError(t, err, "A deny-all policy should be created when the feature is enabled without rules") { + return + } + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + policy.Spec.PolicyTypes, "Both directions should be denied when no rules are configured") +} + +// TestSyncUpdatesPolicyWhenAllRulesAreCleared covers that clearing the rules tightens the +// existing policy into a deny-all one rather than leaving the previous rules on the cluster. +func TestSyncUpdatesPolicyWhenAllRulesAreCleared(t *testing.T) { + testdw := getTestDevWorkspace("test-devworkspace", enabledConfig()) + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := SyncNetworkPolicy(testdw, api) + assert.Error(t, err, "Should return RetryError to indicate that the policy was created") + err = SyncNetworkPolicy(testdw, api) + assert.NoError(t, err, "Policy should be in sync") + + testdw.Config.Workspace.NetworkPolicy.Egress = nil + testdw.Config.Workspace.NetworkPolicy.Ingress = nil + err = SyncNetworkPolicy(testdw, api) + assert.Error(t, err, "Should return RetryError to indicate that the policy was updated") + err = SyncNetworkPolicy(testdw, api) + assert.NoError(t, err, "Policy should be in sync again") + + policy, err := getPolicy(testdw, api) + if !assert.NoError(t, err, "The policy should still exist") { + return + } + assert.Nil(t, policy.Spec.Egress, "The egress rules removed from the configuration should be removed from the cluster object") + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + policy.Spec.PolicyTypes, "Both directions should be denied once all rules are cleared") +} + +func TestSyncIsTolerantOfMissingPolicyWhenDisabled(t *testing.T) { + npConfig := enabledConfig() + npConfig.Enabled = ptr.To(false) + testdw := getTestDevWorkspace("test-devworkspace", npConfig) + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := SyncNetworkPolicy(testdw, api) + assert.NoError(t, err, "Deleting an absent policy should not return an error") +} diff --git a/test/e2e/pkg/tests/networkpolicy_tests.go b/test/e2e/pkg/tests/networkpolicy_tests.go new file mode 100644 index 000000000..94059bf59 --- /dev/null +++ b/test/e2e/pkg/tests/networkpolicy_tests.go @@ -0,0 +1,233 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package tests + +import ( + "context" + "fmt" + "path/filepath" + "time" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + networkingv1 "k8s.io/api/networking/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + + controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + "github.com/devfile/devworkspace-operator/pkg/common" + "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/devfile/devworkspace-operator/test/e2e/pkg/config" +) + +var _ = ginkgo.Describe("[Workspace NetworkPolicy Tests]", ginkgo.Ordered, func() { + defer ginkgo.GinkgoRecover() + + const workspaceName = "network-policy-test" + + var originalConfig *controllerv1alpha1.OperatorConfiguration + // workspaceId is read from the workspace's status once it is running; the NetworkPolicy + // is named and selects pods by workspace ID rather than by workspace name. + var workspaceId string + + getWorkspacePolicy := func() (*networkingv1.NetworkPolicy, error) { + policy := &networkingv1.NetworkPolicy{} + err := config.AdminK8sClient.ControllerRuntimeClient().Get(context.Background(), types.NamespacedName{ + Name: common.NetworkPolicyName(workspaceId), + Namespace: config.DevWorkspaceNamespace, + }, policy) + return policy, err + } + + // setNetworkPolicyConfig updates the network policy section of the global DWOC. A nil + // npConfig removes the section entirely, which leaves provisioning disabled. + setNetworkPolicyConfig := func(npConfig *controllerv1alpha1.NetworkPolicyConfig) { + ctx := context.Background() + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := config.AdminK8sClient.ControllerRuntimeClient().Get(ctx, types.NamespacedName{ + Name: "devworkspace-operator-config", + Namespace: config.OperatorNamespace, + }, dwoc) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to get DWOC: %s", err)) + } + if dwoc.Config == nil { + dwoc.Config = &controllerv1alpha1.OperatorConfiguration{} + } + if dwoc.Config.Workspace == nil { + dwoc.Config.Workspace = &controllerv1alpha1.WorkspaceConfig{} + } + dwoc.Config.Workspace.NetworkPolicy = npConfig + if err := config.AdminK8sClient.ControllerRuntimeClient().Update(ctx, dwoc); err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to update DWOC network policy configuration: %s", err)) + } + } + + // forceReconcile triggers a reconcile of the workspace. Changing the DWOC updates the + // operator's resolved configuration but does not enqueue the workspaces it affects, so a + // running workspace would otherwise only pick the new configuration up on its next + // unrelated event. + forceReconcile := func() { + patch := []byte(fmt.Sprintf(`{"metadata":{"annotations":{"force-update":"%d"}}}`, time.Now().UnixNano())) + workspace := &dw.DevWorkspace{ + ObjectMeta: metav1.ObjectMeta{ + Name: workspaceName, + Namespace: config.DevWorkspaceNamespace, + }, + } + err := config.DevK8sClient.ControllerRuntimeClient().Patch(context.Background(), workspace, crclient.RawPatch(types.MergePatchType, patch)) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to trigger a reconcile of the workspace: %s", err)) + } + } + + enabledConfig := func() *controllerv1alpha1.NetworkPolicyConfig { + return &controllerv1alpha1.NetworkPolicyConfig{ + Enabled: pointer.Bool(true), + Egress: []networkingv1.NetworkPolicyEgressRule{{}}, + } + } + + ginkgo.BeforeAll(func() { + // Save original DWOC configuration to restore after tests + ctx := context.Background() + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := config.AdminK8sClient.ControllerRuntimeClient().Get(ctx, types.NamespacedName{ + Name: "devworkspace-operator-config", + Namespace: config.OperatorNamespace, + }, dwoc) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to get original DWOC: %s", err)) + } + if dwoc.Config != nil { + originalConfig = dwoc.Config.DeepCopy() + } + + // The workspace is created while provisioning is disabled, so that the tests below + // cover enabling the feature for a workspace that already exists. + setNetworkPolicyConfig(nil) + }) + + ginkgo.AfterAll(func() { + // Clean up workspace and wait for PVC to be fully deleted + // This prevents PVC conflicts in subsequent tests, especially in CI environments + _ = config.DevK8sClient.DeleteDevWorkspaceAndWait(workspaceName, config.DevWorkspaceNamespace) + + // Restore original DWOC configuration to prevent config leaks between test runs + ctx := context.Background() + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := config.AdminK8sClient.ControllerRuntimeClient().Get(ctx, types.NamespacedName{ + Name: "devworkspace-operator-config", + Namespace: config.OperatorNamespace, + }, dwoc) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to get current DWOC for restoration: %s", err)) + } + dwoc.Config = originalConfig + if err := config.AdminK8sClient.ControllerRuntimeClient().Update(ctx, dwoc); err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to restore original DWOC configuration: %s", err)) + } + }) + + ginkgo.It("Creates no NetworkPolicy while provisioning is disabled", func() { + workspaceFile := filepath.Join(getProjectRoot(), "test", "resources", "network-policy-test-workspace.yaml") + commandResult, err := config.DevK8sClient.OcApplyWorkspace(config.DevWorkspaceNamespace, workspaceFile) + if err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to create workspace: %s %s", err.Error(), commandResult)) + return + } + + deploy, err := config.DevK8sClient.WaitDevWsStatus(workspaceName, config.DevWorkspaceNamespace, dw.DevWorkspaceStatusRunning) + if !deploy { + ginkgo.Fail(fmt.Sprintf("Workspace didn't start properly. Error: %s", err)) + } + + status, err := config.DevK8sClient.GetDevWsStatus(workspaceName, config.DevWorkspaceNamespace) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + workspaceId = status.DevWorkspaceId + gomega.Expect(workspaceId).NotTo(gomega.BeEmpty(), "Running workspace should have a workspace ID") + + gomega.Consistently(func() bool { + _, err := getWorkspacePolicy() + return k8sErrors.IsNotFound(err) + }, "15s", "5s").Should(gomega.BeTrue(), "No NetworkPolicy should be created while provisioning is disabled") + }) + + ginkgo.It("Creates a NetworkPolicy owned by an already running workspace when provisioning is enabled", func() { + setNetworkPolicyConfig(enabledConfig()) + forceReconcile() + + gomega.Eventually(func() error { + _, err := getWorkspacePolicy() + return err + }, "2m", "5s").Should(gomega.Succeed(), "NetworkPolicy should be created for the existing workspace") + + policy, err := getWorkspacePolicy() + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(policy.Spec.PodSelector.MatchLabels).To(gomega.HaveKeyWithValue(constants.DevWorkspaceIDLabel, workspaceId), + "Policy should select only the pods of its own workspace") + gomega.Expect(policy.Spec.PolicyTypes).To(gomega.ContainElement(networkingv1.PolicyTypeEgress)) + + // The ownerReference is what cleans the policy up; there is no finalizer on the + // DevWorkspace for it. + gomega.Expect(policy.OwnerReferences).To(gomega.HaveLen(1), "Policy should be owned by its workspace") + ownerref := policy.OwnerReferences[0] + gomega.Expect(ownerref.Kind).To(gomega.Equal("DevWorkspace")) + gomega.Expect(ownerref.Name).To(gomega.Equal(workspaceName)) + gomega.Expect(pointer.BoolDeref(ownerref.Controller, false)).To(gomega.BeTrue()) + + workspace := &dw.DevWorkspace{} + err = config.DevK8sClient.ControllerRuntimeClient().Get(context.Background(), types.NamespacedName{ + Name: workspaceName, + Namespace: config.DevWorkspaceNamespace, + }, workspace) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(workspace.Finalizers).NotTo(gomega.ContainElement(gomega.ContainSubstring("networkpolicy")), + "NetworkPolicy cleanup should not rely on a finalizer") + }) + + ginkgo.It("Removes the NetworkPolicy when provisioning is disabled", func() { + setNetworkPolicyConfig(nil) + forceReconcile() + + gomega.Eventually(func() bool { + _, err := getWorkspacePolicy() + return k8sErrors.IsNotFound(err) + }, "2m", "5s").Should(gomega.BeTrue(), "NetworkPolicy should be removed when provisioning is disabled") + }) + + ginkgo.It("Garbage collects the NetworkPolicy when the workspace is deleted", func() { + setNetworkPolicyConfig(enabledConfig()) + forceReconcile() + + gomega.Eventually(func() error { + _, err := getWorkspacePolicy() + return err + }, "2m", "5s").Should(gomega.Succeed(), "NetworkPolicy should be created again once provisioning is re-enabled") + + err := config.DevK8sClient.DeleteDevWorkspaceAndWait(workspaceName, config.DevWorkspaceNamespace) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to delete DevWorkspace") + + gomega.Eventually(func() bool { + _, err := getWorkspacePolicy() + return k8sErrors.IsNotFound(err) + }, "2m", "5s").Should(gomega.BeTrue(), "NetworkPolicy should be garbage collected once its workspace is deleted") + }) +}) diff --git a/test/resources/network-policy-test-workspace.yaml b/test/resources/network-policy-test-workspace.yaml new file mode 100644 index 000000000..fe49c450a --- /dev/null +++ b/test/resources/network-policy-test-workspace.yaml @@ -0,0 +1,15 @@ +kind: DevWorkspace +apiVersion: workspace.devfile.io/v1alpha2 +metadata: + name: network-policy-test +spec: + started: true + routingClass: 'basic' + template: + attributes: + controller.devfile.io/storage-type: per-user + components: + - name: tooling + container: + image: quay.io/wto/web-terminal-tooling:latest + args: ["tail", "-f", "/dev/null"] From 60ffe3230ddaca4d84fcdf6148d46f64877e1c05 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Mon, 21 Sep 2026 11:37:18 +0200 Subject: [PATCH 2/7] Fixup Signed-off-by: Anatolii Bazko --- .../networkpolicy/networkpolicy_test.go | 329 ++++-------------- 1 file changed, 72 insertions(+), 257 deletions(-) diff --git a/pkg/provision/workspace/networkpolicy/networkpolicy_test.go b/pkg/provision/workspace/networkpolicy/networkpolicy_test.go index cb8c18e99..82846561f 100644 --- a/pkg/provision/workspace/networkpolicy/networkpolicy_test.go +++ b/pkg/provision/workspace/networkpolicy/networkpolicy_test.go @@ -19,31 +19,30 @@ import ( "testing" dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + "github.com/devfile/devworkspace-operator/pkg/common" "github.com/devfile/devworkspace-operator/pkg/config" + "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/devfile/devworkspace-operator/pkg/dwerrors" + "github.com/devfile/devworkspace-operator/pkg/provision/sync" "github.com/go-logr/logr/testr" "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" - "github.com/devfile/devworkspace-operator/pkg/common" - "github.com/devfile/devworkspace-operator/pkg/constants" - "github.com/devfile/devworkspace-operator/pkg/dwerrors" - "github.com/devfile/devworkspace-operator/pkg/provision/sync" ) -const testNamespace = "test-namespace" +const ( + testNamespace = "test-namespace" + testDevworkspaceName = "test-devworkspace" +) var scheme = runtime.NewScheme() @@ -63,11 +62,7 @@ func getTestClusterAPI(t *testing.T, initialObjects ...client.Object) sync.Clust } } -// getTestDevWorkspace returns a DevWorkspace whose resolved config carries the given -// network policy configuration. A nil npConfig leaves the section unset. The workspace ID -// deliberately differs from the workspace name, so that tests cannot pass by using one -// where the other is expected. -func getTestDevWorkspace(name string, npConfig *v1alpha1.NetworkPolicyConfig) *common.DevWorkspaceWithConfig { +func getTestDevWorkspaceWithConfig(name string, npConfig *v1alpha1.NetworkPolicyConfig) *common.DevWorkspaceWithConfig { return &common.DevWorkspaceWithConfig{ DevWorkspace: &dw.DevWorkspace{ ObjectMeta: metav1.ObjectMeta{ @@ -87,52 +82,42 @@ func getTestDevWorkspace(name string, npConfig *v1alpha1.NetworkPolicyConfig) *c } } -func enabledConfig() *v1alpha1.NetworkPolicyConfig { +func getEnabledNetworkPolicyConfig() *v1alpha1.NetworkPolicyConfig { return &v1alpha1.NetworkPolicyConfig{ - Enabled: ptr.To(true), - Egress: []networkingv1.NetworkPolicyEgressRule{{}}, + Enabled: new(true), } } -// getPolicy reads the NetworkPolicy belonging to a workspace from the cluster. -func getPolicy(testdw *common.DevWorkspaceWithConfig, api sync.ClusterAPI) (*networkingv1.NetworkPolicy, error) { +func getNetworkPolicyFromCluster(testDevworkspace *common.DevWorkspaceWithConfig, api sync.ClusterAPI) (*networkingv1.NetworkPolicy, error) { actual := &networkingv1.NetworkPolicy{} - err := api.Client.Get(api.Ctx, types.NamespacedName{ - Name: common.NetworkPolicyName(testdw.Status.DevWorkspaceId), - Namespace: testdw.Namespace, - }, actual) + err := api.Client.Get( + api.Ctx, + types.NamespacedName{ + Name: common.NetworkPolicyName(testDevworkspace.Status.DevWorkspaceId), + Namespace: testDevworkspace.Namespace, + }, + actual) + return actual, err } func TestGeneratedPolicySelectsOnlyItsOwnWorkspacePods(t *testing.T) { - testdw := getTestDevWorkspace("test-devworkspace", enabledConfig()) - policy := generateNetworkPolicy(testdw, enabledConfig()) - assert.Equal(t, common.NetworkPolicyName(testdw.Status.DevWorkspaceId), policy.Name, "Policy should be named after the workspace it governs") - assert.Equal(t, testNamespace, policy.Namespace, "Policy should be created in the workspace namespace") + testDevWorkspace := getTestDevWorkspaceWithConfig(testDevworkspaceName, getEnabledNetworkPolicyConfig()) + policy := generateNetworkPolicy(testDevWorkspace, getEnabledNetworkPolicyConfig()) + + assert.Equal(t, common.NetworkPolicyName(testDevWorkspace.Status.DevWorkspaceId), policy.Name) + assert.Equal(t, testNamespace, policy.Namespace) assert.Equal(t, map[string]string{ - constants.DevWorkspaceIDLabel: testdw.Status.DevWorkspaceId, - constants.DevWorkspaceNameLabel: testdw.Name, - }, policy.Labels, "Policy should carry the labels every per-workspace object carries, so that it is watched by the controller cache") + constants.DevWorkspaceIDLabel: testDevWorkspace.Status.DevWorkspaceId, + constants.DevWorkspaceNameLabel: testDevWorkspace.Name, + }, policy.Labels) assert.Equal(t, metav1.LabelSelector{ MatchLabels: map[string]string{ - constants.DevWorkspaceIDLabel: testdw.Status.DevWorkspaceId, + constants.DevWorkspaceIDLabel: testDevWorkspace.Status.DevWorkspaceId, }, - }, policy.Spec.PodSelector, "Policy should select only the pods of its own workspace") + }, policy.Spec.PodSelector) } -func TestGeneratedPoliciesForDifferentWorkspacesDoNotCollide(t *testing.T) { - first := getTestDevWorkspace("first-devworkspace", enabledConfig()) - second := getTestDevWorkspace("second-devworkspace", enabledConfig()) - firstPolicy := generateNetworkPolicy(first, enabledConfig()) - secondPolicy := generateNetworkPolicy(second, enabledConfig()) - assert.NotEqual(t, firstPolicy.Name, secondPolicy.Name, - "Each workspace should get its own policy, otherwise two workspaces in a namespace would fight over one object") - assert.NotEqual(t, firstPolicy.Spec.PodSelector, secondPolicy.Spec.PodSelector, - "Each policy should select only its own workspace's pods") -} - -// TestUnsetIngressDeniesAllIngress covers that both directions are always listed in -// policyTypes: a direction with no configured rules is denied, not left unrestricted. func TestUnsetIngressDeniesAllIngress(t *testing.T) { tests := []struct { name string @@ -141,249 +126,79 @@ func TestUnsetIngressDeniesAllIngress(t *testing.T) { {name: "nil ingress list", ingress: nil}, {name: "empty ingress list", ingress: []networkingv1.NetworkPolicyIngressRule{}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { npConfig := &v1alpha1.NetworkPolicyConfig{ - Enabled: ptr.To(true), + Enabled: new(true), Ingress: tt.ingress, Egress: []networkingv1.NetworkPolicyEgressRule{{}}, } - policy := generateNetworkPolicy(getTestDevWorkspace("test-devworkspace", npConfig), npConfig) - assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, - policy.Spec.PolicyTypes, "Both directions should always be in policyTypes, so that a direction without rules denies all traffic") - assert.Nil(t, policy.Spec.Ingress, - "A direction without rules must serialize as nil, since an empty slice is dropped by omitempty and would never match the cluster object") + policy := generateNetworkPolicy(getTestDevWorkspaceWithConfig(testDevworkspaceName, npConfig), npConfig) + + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, policy.Spec.PolicyTypes) + assert.Nil(t, policy.Spec.Ingress) }) } } func TestBothDirectionsNilDeniesAllTraffic(t *testing.T) { - npConfig := &v1alpha1.NetworkPolicyConfig{Enabled: ptr.To(true)} - policy := generateNetworkPolicy(getTestDevWorkspace("test-devworkspace", npConfig), npConfig) - if !assert.NotNil(t, policy, "A policy should be built even when no rules are configured") { - return - } - assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, - policy.Spec.PolicyTypes, "With no rules configured both directions should be denied") - assert.Nil(t, policy.Spec.Ingress, "No ingress rules should be produced") - assert.Nil(t, policy.Spec.Egress, "No egress rules should be produced") -} - -func TestPortProtocolIsDefaultedToTCP(t *testing.T) { - port := intstr.FromInt(8080) - npConfig := &v1alpha1.NetworkPolicyConfig{ - Enabled: ptr.To(true), - Ingress: []networkingv1.NetworkPolicyIngressRule{ - { - Ports: []networkingv1.NetworkPolicyPort{{Port: &port}}, - }, - }, - } - policy := generateNetworkPolicy(getTestDevWorkspace("test-devworkspace", npConfig), npConfig) - actual := policy.Spec.Ingress[0].Ports[0].Protocol - if !assert.NotNil(t, actual, "Protocol should be defaulted so the spec matches what the API server stores") { - return - } - assert.Equal(t, corev1.ProtocolTCP, *actual, "Omitted protocol should default to TCP") -} - -func TestExplicitPortProtocolIsPreserved(t *testing.T) { - port := intstr.FromInt(53) - udp := corev1.ProtocolUDP - npConfig := &v1alpha1.NetworkPolicyConfig{ - Enabled: ptr.To(true), - Egress: []networkingv1.NetworkPolicyEgressRule{ - { - Ports: []networkingv1.NetworkPolicyPort{{Port: &port, Protocol: &udp}}, - }, - }, - } - policy := generateNetworkPolicy(getTestDevWorkspace("test-devworkspace", npConfig), npConfig) - assert.Equal(t, corev1.ProtocolUDP, *policy.Spec.Egress[0].Ports[0].Protocol, "Explicit protocol should be preserved") -} + npConfig := &v1alpha1.NetworkPolicyConfig{Enabled: new(true)} + policy := generateNetworkPolicy(getTestDevWorkspaceWithConfig(testDevworkspaceName, npConfig), npConfig) -func TestGenerateDoesNotMutateConfig(t *testing.T) { - port := intstr.FromInt(8080) - npConfig := &v1alpha1.NetworkPolicyConfig{ - Enabled: ptr.To(true), - Ingress: []networkingv1.NetworkPolicyIngressRule{ - { - Ports: []networkingv1.NetworkPolicyPort{{Port: &port}}, - }, - }, - } - generateNetworkPolicy(getTestDevWorkspace("test-devworkspace", npConfig), npConfig) - assert.Nil(t, npConfig.Ingress[0].Ports[0].Protocol, - "Normalization must not write back into the shared resolved config") + assert.NotNil(t, policy) + assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, policy.Spec.PolicyTypes) + assert.Nil(t, policy.Spec.Ingress) + assert.Nil(t, policy.Spec.Egress) } func TestSyncCreatesPolicyWhenEnabled(t *testing.T) { - testdw := getTestDevWorkspace("test-devworkspace", enabledConfig()) - api := getTestClusterAPI(t, testdw.DevWorkspace) - err := SyncNetworkPolicy(testdw, api) - retryErr := &dwerrors.RetryError{} - if assert.Error(t, err, "Should return RetryError to indicate that the policy was created") { - assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") - } - err = SyncNetworkPolicy(testdw, api) - assert.NoError(t, err, "Should not return error once the policy is in sync") - - _, err = getPolicy(testdw, api) - assert.NoError(t, err, "Policy should exist on the cluster") -} - -// TestSyncOwnsPolicyByItsWorkspace covers the reason no finalizer is needed: the policy is -// garbage collected by the cluster along with the DevWorkspace that owns it. -func TestSyncOwnsPolicyByItsWorkspace(t *testing.T) { - testdw := getTestDevWorkspace("test-devworkspace", enabledConfig()) - api := getTestClusterAPI(t, testdw.DevWorkspace) - err := SyncNetworkPolicy(testdw, api) - assert.Error(t, err, "Should return RetryError to indicate that the policy was created") - - policy, err := getPolicy(testdw, api) - if !assert.NoError(t, err, "Policy should exist on the cluster") { - return - } - if !assert.Len(t, policy.OwnerReferences, 1, "Policy should be owned by the workspace it governs, so that it is garbage collected with it") { - return - } - ownerref := policy.OwnerReferences[0] - assert.Equal(t, testdw.Name, ownerref.Name, "Policy should be owned by its own workspace") - assert.Equal(t, testdw.UID, ownerref.UID, "Policy should be owned by its own workspace") - assert.Equal(t, "DevWorkspace", ownerref.Kind, "Policy should be owned by its own workspace") - assert.True(t, ptr.Deref(ownerref.Controller, false), "Workspace should be the controller of its policy") -} - -// TestSyncTreatsApiServerDefaultedProtocolAsInSync covers the round trip normalizePorts -// exists for: the configuration omits the port protocol, while the object stored on the -// cluster carries the protocol the API server defaulted. Without normalization the two -// never compare equal and every reconcile requests another update. -func TestSyncTreatsApiServerDefaultedProtocolAsInSync(t *testing.T) { - port := intstr.FromInt(8080) - tcp := corev1.ProtocolTCP - npConfig := &v1alpha1.NetworkPolicyConfig{ - Enabled: ptr.To(true), - Ingress: []networkingv1.NetworkPolicyIngressRule{ - { - Ports: []networkingv1.NetworkPolicyPort{{Port: &port}}, - }, - }, - } + testDevworkspace := getTestDevWorkspaceWithConfig(testDevworkspaceName, getEnabledNetworkPolicyConfig()) + api := getTestClusterAPI(t, testDevworkspace.DevWorkspace) - testdw := getTestDevWorkspace("test-devworkspace", npConfig) - defaultedByApiServer := generateNetworkPolicy(testdw, &v1alpha1.NetworkPolicyConfig{ - Enabled: ptr.To(true), - Ingress: []networkingv1.NetworkPolicyIngressRule{ - { - Ports: []networkingv1.NetworkPolicyPort{{Port: &port, Protocol: &tcp}}, - }, - }, - }) + err := SyncNetworkPolicy(testDevworkspace, api) + retryErr := &dwerrors.RetryError{} + assert.Error(t, err) + assert.ErrorAs(t, err, &retryErr) - // The policy on the cluster was created by a previous sync, so it already carries the - // ownerReference; without it the sync would request an update over the missing ownerref - // rather than over the protocol under test. - err := controllerutil.SetControllerReference(testdw.DevWorkspace, defaultedByApiServer, scheme) + err = SyncNetworkPolicy(testDevworkspace, api) assert.NoError(t, err) - api := getTestClusterAPI(t, testdw.DevWorkspace, defaultedByApiServer) - err = SyncNetworkPolicy(testdw, api) - assert.NoError(t, err, - "A policy stored with the protocol defaulted by the API server should be considered in sync, otherwise every reconcile requests an update") -} - -func TestSyncDoesNothingWhenConfigSectionIsUnset(t *testing.T) { - testdw := getTestDevWorkspace("test-devworkspace", nil) - testdw.Config.Workspace.NetworkPolicy = nil - api := getTestClusterAPI(t, testdw.DevWorkspace) - err := SyncNetworkPolicy(testdw, api) - assert.NoError(t, err, "Should not return error when no network policy is configured") + policy, err := getNetworkPolicyFromCluster(testDevworkspace, api) + assert.NoError(t, err) - _, err = getPolicy(testdw, api) - assert.True(t, k8sErrors.IsNotFound(err), "No policy should be created") + assert.Equal(t, testDevworkspace.Name, policy.OwnerReferences[0].Name) + assert.Equal(t, testDevworkspace.UID, policy.OwnerReferences[0].UID) + assert.Equal(t, "DevWorkspace", policy.OwnerReferences[0].Kind) + assert.True(t, ptr.Deref(policy.OwnerReferences[0].Controller, false)) } func TestSyncDeletesPolicyWhenDisabled(t *testing.T) { - testdw := getTestDevWorkspace("test-devworkspace", enabledConfig()) - api := getTestClusterAPI(t, testdw.DevWorkspace) - err := SyncNetworkPolicy(testdw, api) - assert.Error(t, err, "Should return RetryError to indicate that the policy was created") - err = SyncNetworkPolicy(testdw, api) - assert.NoError(t, err, "Policy should be in sync") - - testdw.Config.Workspace.NetworkPolicy.Enabled = ptr.To(false) - err = SyncNetworkPolicy(testdw, api) - assert.NoError(t, err, "Disabling should not return an error") - - _, err = getPolicy(testdw, api) - assert.True(t, k8sErrors.IsNotFound(err), "Policy should be removed when the feature is disabled") -} + testDevworkspace := getTestDevWorkspaceWithConfig(testDevworkspaceName, getEnabledNetworkPolicyConfig()) + api := getTestClusterAPI(t, testDevworkspace.DevWorkspace) -// TestSyncLeavesOtherWorkspacesPolicyAlone guards the per-workspace scope of the delete: a -// workspace that has the feature disabled must not remove the policy governing a different -// workspace in the same namespace. -func TestSyncLeavesOtherWorkspacesPolicyAlone(t *testing.T) { - restricted := getTestDevWorkspace("restricted-devworkspace", enabledConfig()) - unrestricted := getTestDevWorkspace("unrestricted-devworkspace", nil) - unrestricted.Config.Workspace.NetworkPolicy = nil - api := getTestClusterAPI(t, restricted.DevWorkspace, unrestricted.DevWorkspace) - err := SyncNetworkPolicy(restricted, api) - assert.Error(t, err, "Should return RetryError to indicate that the policy was created") - - err = SyncNetworkPolicy(unrestricted, api) - assert.NoError(t, err, "Syncing a workspace with no policy configured should not return an error") - - _, err = getPolicy(restricted, api) - assert.NoError(t, err, "A workspace must not delete the policy that governs another workspace") -} - -func TestSyncCreatesDenyAllPolicyWhenNoRulesAreConfigured(t *testing.T) { - testdw := getTestDevWorkspace("test-devworkspace", &v1alpha1.NetworkPolicyConfig{Enabled: ptr.To(true)}) - api := getTestClusterAPI(t, testdw.DevWorkspace) - err := SyncNetworkPolicy(testdw, api) - assert.Error(t, err, "Should return RetryError to indicate that the policy was created") - err = SyncNetworkPolicy(testdw, api) - assert.NoError(t, err, "Should not return error once the policy is in sync") + err := SyncNetworkPolicy(testDevworkspace, api) + assert.Error(t, err) - policy, err := getPolicy(testdw, api) - if !assert.NoError(t, err, "A deny-all policy should be created when the feature is enabled without rules") { - return - } - assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, - policy.Spec.PolicyTypes, "Both directions should be denied when no rules are configured") -} + err = SyncNetworkPolicy(testDevworkspace, api) + assert.NoError(t, err) -// TestSyncUpdatesPolicyWhenAllRulesAreCleared covers that clearing the rules tightens the -// existing policy into a deny-all one rather than leaving the previous rules on the cluster. -func TestSyncUpdatesPolicyWhenAllRulesAreCleared(t *testing.T) { - testdw := getTestDevWorkspace("test-devworkspace", enabledConfig()) - api := getTestClusterAPI(t, testdw.DevWorkspace) - err := SyncNetworkPolicy(testdw, api) - assert.Error(t, err, "Should return RetryError to indicate that the policy was created") - err = SyncNetworkPolicy(testdw, api) - assert.NoError(t, err, "Policy should be in sync") + _, err = getNetworkPolicyFromCluster(testDevworkspace, api) + assert.NoError(t, err) - testdw.Config.Workspace.NetworkPolicy.Egress = nil - testdw.Config.Workspace.NetworkPolicy.Ingress = nil - err = SyncNetworkPolicy(testdw, api) - assert.Error(t, err, "Should return RetryError to indicate that the policy was updated") - err = SyncNetworkPolicy(testdw, api) - assert.NoError(t, err, "Policy should be in sync again") + testDevworkspace.Config.Workspace.NetworkPolicy.Enabled = new(false) + err = SyncNetworkPolicy(testDevworkspace, api) + assert.NoError(t, err) - policy, err := getPolicy(testdw, api) - if !assert.NoError(t, err, "The policy should still exist") { - return - } - assert.Nil(t, policy.Spec.Egress, "The egress rules removed from the configuration should be removed from the cluster object") - assert.Equal(t, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, - policy.Spec.PolicyTypes, "Both directions should be denied once all rules are cleared") + _, err = getNetworkPolicyFromCluster(testDevworkspace, api) + assert.True(t, k8sErrors.IsNotFound(err)) } func TestSyncIsTolerantOfMissingPolicyWhenDisabled(t *testing.T) { - npConfig := enabledConfig() + npConfig := getEnabledNetworkPolicyConfig() npConfig.Enabled = ptr.To(false) - testdw := getTestDevWorkspace("test-devworkspace", npConfig) + testdw := getTestDevWorkspaceWithConfig("test-devworkspace", npConfig) api := getTestClusterAPI(t, testdw.DevWorkspace) err := SyncNetworkPolicy(testdw, api) - assert.NoError(t, err, "Deleting an absent policy should not return an error") + assert.NoError(t, err) } From d005741f7873f2ee192ceb38cd8682d8fa0511c4 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Wed, 23 Sep 2026 09:31:56 +0200 Subject: [PATCH 3/7] remove e2e test Signed-off-by: Anatolii Bazko --- test/e2e/pkg/tests/networkpolicy_tests.go | 233 ------------------ .../network-policy-test-workspace.yaml | 15 -- 2 files changed, 248 deletions(-) delete mode 100644 test/e2e/pkg/tests/networkpolicy_tests.go delete mode 100644 test/resources/network-policy-test-workspace.yaml diff --git a/test/e2e/pkg/tests/networkpolicy_tests.go b/test/e2e/pkg/tests/networkpolicy_tests.go deleted file mode 100644 index 94059bf59..000000000 --- a/test/e2e/pkg/tests/networkpolicy_tests.go +++ /dev/null @@ -1,233 +0,0 @@ -// -// Copyright (c) 2019-2026 Red Hat, Inc. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -package tests - -import ( - "context" - "fmt" - "path/filepath" - "time" - - dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" - "github.com/onsi/ginkgo/v2" - "github.com/onsi/gomega" - networkingv1 "k8s.io/api/networking/v1" - k8sErrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/pointer" - crclient "sigs.k8s.io/controller-runtime/pkg/client" - - controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" - "github.com/devfile/devworkspace-operator/pkg/common" - "github.com/devfile/devworkspace-operator/pkg/constants" - "github.com/devfile/devworkspace-operator/test/e2e/pkg/config" -) - -var _ = ginkgo.Describe("[Workspace NetworkPolicy Tests]", ginkgo.Ordered, func() { - defer ginkgo.GinkgoRecover() - - const workspaceName = "network-policy-test" - - var originalConfig *controllerv1alpha1.OperatorConfiguration - // workspaceId is read from the workspace's status once it is running; the NetworkPolicy - // is named and selects pods by workspace ID rather than by workspace name. - var workspaceId string - - getWorkspacePolicy := func() (*networkingv1.NetworkPolicy, error) { - policy := &networkingv1.NetworkPolicy{} - err := config.AdminK8sClient.ControllerRuntimeClient().Get(context.Background(), types.NamespacedName{ - Name: common.NetworkPolicyName(workspaceId), - Namespace: config.DevWorkspaceNamespace, - }, policy) - return policy, err - } - - // setNetworkPolicyConfig updates the network policy section of the global DWOC. A nil - // npConfig removes the section entirely, which leaves provisioning disabled. - setNetworkPolicyConfig := func(npConfig *controllerv1alpha1.NetworkPolicyConfig) { - ctx := context.Background() - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := config.AdminK8sClient.ControllerRuntimeClient().Get(ctx, types.NamespacedName{ - Name: "devworkspace-operator-config", - Namespace: config.OperatorNamespace, - }, dwoc) - if err != nil { - ginkgo.Fail(fmt.Sprintf("Failed to get DWOC: %s", err)) - } - if dwoc.Config == nil { - dwoc.Config = &controllerv1alpha1.OperatorConfiguration{} - } - if dwoc.Config.Workspace == nil { - dwoc.Config.Workspace = &controllerv1alpha1.WorkspaceConfig{} - } - dwoc.Config.Workspace.NetworkPolicy = npConfig - if err := config.AdminK8sClient.ControllerRuntimeClient().Update(ctx, dwoc); err != nil { - ginkgo.Fail(fmt.Sprintf("Failed to update DWOC network policy configuration: %s", err)) - } - } - - // forceReconcile triggers a reconcile of the workspace. Changing the DWOC updates the - // operator's resolved configuration but does not enqueue the workspaces it affects, so a - // running workspace would otherwise only pick the new configuration up on its next - // unrelated event. - forceReconcile := func() { - patch := []byte(fmt.Sprintf(`{"metadata":{"annotations":{"force-update":"%d"}}}`, time.Now().UnixNano())) - workspace := &dw.DevWorkspace{ - ObjectMeta: metav1.ObjectMeta{ - Name: workspaceName, - Namespace: config.DevWorkspaceNamespace, - }, - } - err := config.DevK8sClient.ControllerRuntimeClient().Patch(context.Background(), workspace, crclient.RawPatch(types.MergePatchType, patch)) - if err != nil { - ginkgo.Fail(fmt.Sprintf("Failed to trigger a reconcile of the workspace: %s", err)) - } - } - - enabledConfig := func() *controllerv1alpha1.NetworkPolicyConfig { - return &controllerv1alpha1.NetworkPolicyConfig{ - Enabled: pointer.Bool(true), - Egress: []networkingv1.NetworkPolicyEgressRule{{}}, - } - } - - ginkgo.BeforeAll(func() { - // Save original DWOC configuration to restore after tests - ctx := context.Background() - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := config.AdminK8sClient.ControllerRuntimeClient().Get(ctx, types.NamespacedName{ - Name: "devworkspace-operator-config", - Namespace: config.OperatorNamespace, - }, dwoc) - if err != nil { - ginkgo.Fail(fmt.Sprintf("Failed to get original DWOC: %s", err)) - } - if dwoc.Config != nil { - originalConfig = dwoc.Config.DeepCopy() - } - - // The workspace is created while provisioning is disabled, so that the tests below - // cover enabling the feature for a workspace that already exists. - setNetworkPolicyConfig(nil) - }) - - ginkgo.AfterAll(func() { - // Clean up workspace and wait for PVC to be fully deleted - // This prevents PVC conflicts in subsequent tests, especially in CI environments - _ = config.DevK8sClient.DeleteDevWorkspaceAndWait(workspaceName, config.DevWorkspaceNamespace) - - // Restore original DWOC configuration to prevent config leaks between test runs - ctx := context.Background() - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := config.AdminK8sClient.ControllerRuntimeClient().Get(ctx, types.NamespacedName{ - Name: "devworkspace-operator-config", - Namespace: config.OperatorNamespace, - }, dwoc) - if err != nil { - ginkgo.Fail(fmt.Sprintf("Failed to get current DWOC for restoration: %s", err)) - } - dwoc.Config = originalConfig - if err := config.AdminK8sClient.ControllerRuntimeClient().Update(ctx, dwoc); err != nil { - ginkgo.Fail(fmt.Sprintf("Failed to restore original DWOC configuration: %s", err)) - } - }) - - ginkgo.It("Creates no NetworkPolicy while provisioning is disabled", func() { - workspaceFile := filepath.Join(getProjectRoot(), "test", "resources", "network-policy-test-workspace.yaml") - commandResult, err := config.DevK8sClient.OcApplyWorkspace(config.DevWorkspaceNamespace, workspaceFile) - if err != nil { - ginkgo.Fail(fmt.Sprintf("Failed to create workspace: %s %s", err.Error(), commandResult)) - return - } - - deploy, err := config.DevK8sClient.WaitDevWsStatus(workspaceName, config.DevWorkspaceNamespace, dw.DevWorkspaceStatusRunning) - if !deploy { - ginkgo.Fail(fmt.Sprintf("Workspace didn't start properly. Error: %s", err)) - } - - status, err := config.DevK8sClient.GetDevWsStatus(workspaceName, config.DevWorkspaceNamespace) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - workspaceId = status.DevWorkspaceId - gomega.Expect(workspaceId).NotTo(gomega.BeEmpty(), "Running workspace should have a workspace ID") - - gomega.Consistently(func() bool { - _, err := getWorkspacePolicy() - return k8sErrors.IsNotFound(err) - }, "15s", "5s").Should(gomega.BeTrue(), "No NetworkPolicy should be created while provisioning is disabled") - }) - - ginkgo.It("Creates a NetworkPolicy owned by an already running workspace when provisioning is enabled", func() { - setNetworkPolicyConfig(enabledConfig()) - forceReconcile() - - gomega.Eventually(func() error { - _, err := getWorkspacePolicy() - return err - }, "2m", "5s").Should(gomega.Succeed(), "NetworkPolicy should be created for the existing workspace") - - policy, err := getWorkspacePolicy() - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(policy.Spec.PodSelector.MatchLabels).To(gomega.HaveKeyWithValue(constants.DevWorkspaceIDLabel, workspaceId), - "Policy should select only the pods of its own workspace") - gomega.Expect(policy.Spec.PolicyTypes).To(gomega.ContainElement(networkingv1.PolicyTypeEgress)) - - // The ownerReference is what cleans the policy up; there is no finalizer on the - // DevWorkspace for it. - gomega.Expect(policy.OwnerReferences).To(gomega.HaveLen(1), "Policy should be owned by its workspace") - ownerref := policy.OwnerReferences[0] - gomega.Expect(ownerref.Kind).To(gomega.Equal("DevWorkspace")) - gomega.Expect(ownerref.Name).To(gomega.Equal(workspaceName)) - gomega.Expect(pointer.BoolDeref(ownerref.Controller, false)).To(gomega.BeTrue()) - - workspace := &dw.DevWorkspace{} - err = config.DevK8sClient.ControllerRuntimeClient().Get(context.Background(), types.NamespacedName{ - Name: workspaceName, - Namespace: config.DevWorkspaceNamespace, - }, workspace) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - gomega.Expect(workspace.Finalizers).NotTo(gomega.ContainElement(gomega.ContainSubstring("networkpolicy")), - "NetworkPolicy cleanup should not rely on a finalizer") - }) - - ginkgo.It("Removes the NetworkPolicy when provisioning is disabled", func() { - setNetworkPolicyConfig(nil) - forceReconcile() - - gomega.Eventually(func() bool { - _, err := getWorkspacePolicy() - return k8sErrors.IsNotFound(err) - }, "2m", "5s").Should(gomega.BeTrue(), "NetworkPolicy should be removed when provisioning is disabled") - }) - - ginkgo.It("Garbage collects the NetworkPolicy when the workspace is deleted", func() { - setNetworkPolicyConfig(enabledConfig()) - forceReconcile() - - gomega.Eventually(func() error { - _, err := getWorkspacePolicy() - return err - }, "2m", "5s").Should(gomega.Succeed(), "NetworkPolicy should be created again once provisioning is re-enabled") - - err := config.DevK8sClient.DeleteDevWorkspaceAndWait(workspaceName, config.DevWorkspaceNamespace) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to delete DevWorkspace") - - gomega.Eventually(func() bool { - _, err := getWorkspacePolicy() - return k8sErrors.IsNotFound(err) - }, "2m", "5s").Should(gomega.BeTrue(), "NetworkPolicy should be garbage collected once its workspace is deleted") - }) -}) diff --git a/test/resources/network-policy-test-workspace.yaml b/test/resources/network-policy-test-workspace.yaml deleted file mode 100644 index fe49c450a..000000000 --- a/test/resources/network-policy-test-workspace.yaml +++ /dev/null @@ -1,15 +0,0 @@ -kind: DevWorkspace -apiVersion: workspace.devfile.io/v1alpha2 -metadata: - name: network-policy-test -spec: - started: true - routingClass: 'basic' - template: - attributes: - controller.devfile.io/storage-type: per-user - components: - - name: tooling - container: - image: quay.io/wto/web-terminal-tooling:latest - args: ["tail", "-f", "/dev/null"] From 862d0cb02dbfdfd5408c1160d72804900be370fb Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Fri, 25 Sep 2026 10:44:10 +0200 Subject: [PATCH 4/7] fixup Signed-off-by: Anatolii Bazko --- .../crd/bases/controller.devfile.io_devworkspaceroutings.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml index 025b4c637..ed4178ecb 100644 --- a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml +++ b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: From 89017d38e05b90e1ffdb99d10328b4e649c6b08e Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Fri, 25 Sep 2026 10:50:05 +0200 Subject: [PATCH 5/7] fixup Signed-off-by: Anatolii Bazko --- deploy/templates/components/csv/clusterserviceversion.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/templates/components/csv/clusterserviceversion.yaml b/deploy/templates/components/csv/clusterserviceversion.yaml index 419edc792..16b4215eb 100644 --- a/deploy/templates/components/csv/clusterserviceversion.yaml +++ b/deploy/templates/components/csv/clusterserviceversion.yaml @@ -1,3 +1,4 @@ +--- apiVersion: operators.coreos.com/v1alpha1 kind: ClusterServiceVersion metadata: From 93c84105e700c266e8613c87189be730e477e214 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Mon, 28 Sep 2026 09:03:14 +0200 Subject: [PATCH 6/7] Fixup Signed-off-by: Anatolii Bazko --- pkg/provision/workspace/networkpolicy/networkpolicy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/provision/workspace/networkpolicy/networkpolicy.go b/pkg/provision/workspace/networkpolicy/networkpolicy.go index 48a09cc9a..d8f7ad353 100644 --- a/pkg/provision/workspace/networkpolicy/networkpolicy.go +++ b/pkg/provision/workspace/networkpolicy/networkpolicy.go @@ -32,7 +32,7 @@ import ( ) // generateNetworkPolicy builds the NetworkPolicy applied to a single DevWorkspace's pods. -// The name, labels, podSelector and policyTypes are owned by the operator; only the ingress +// The name, labels, podSelector and policyTypes are defined by the operator; only the ingress // and egress rules come from configuration. Both directions are always listed in policyTypes, // so a direction whose configured rule list is empty denies all traffic in that direction. func generateNetworkPolicy(workspace *common.DevWorkspaceWithConfig, npConfig *v1alpha1.NetworkPolicyConfig) *networkingv1.NetworkPolicy { From 5b7719d843e77f37fe2d4000c6cb26ee6f8666a8 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Wed, 30 Sep 2026 16:20:10 +0200 Subject: [PATCH 7/7] fixup Signed-off-by: Anatolii Bazko --- .../devworkspaceoperatorconfig_types.go | 8 ++-- ...evfile.io_devworkspaceoperatorconfigs.yaml | 8 ++-- deploy/deployment/kubernetes/combined.yaml | 8 ++-- ...r.devfile.io.CustomResourceDefinition.yaml | 8 ++-- deploy/deployment/openshift/combined.yaml | 8 ++-- ...r.devfile.io.CustomResourceDefinition.yaml | 8 ++-- .../components/csv/clusterserviceversion.yaml | 1 - ...evfile.io_devworkspaceoperatorconfigs.yaml | 8 ++-- ...oller.devfile.io_devworkspaceroutings.yaml | 1 - pkg/config/defaults.go | 40 ++++++++++++++----- 10 files changed, 57 insertions(+), 41 deletions(-) diff --git a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go index 5c81d49ef..61741c34d 100644 --- a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go +++ b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go @@ -343,10 +343,10 @@ type NetworkPolicyConfig struct { // Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not // specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, // the defaults allow traffic from the operator's own namespace and from the OpenShift - // monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, - // the default allows all ingress traffic, since the namespace of the cluster's ingress - // controller is not known to the operator; administrators are expected to replace this - // with rules appropriate to their cluster. + // monitoring, ingress, and host-network namespaces, and deny all other ingress traffic. + // On Kubernetes, the default allows all ingress traffic, since the namespace of the + // cluster's ingress controller is not known to the operator; administrators are expected + // to replace this with rules appropriate to their cluster. // If this field is specified as an empty list, all ingress traffic to DevWorkspace pods // is denied. If this field is specified as a non-empty list, exactly those rules apply // and the default rules no longer apply. diff --git a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml index a7418782b..8032de9dd 100644 --- a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -4218,10 +4218,10 @@ spec: Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, the defaults allow traffic from the operator's own namespace and from the OpenShift - monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, - the default allows all ingress traffic, since the namespace of the cluster's ingress - controller is not known to the operator; administrators are expected to replace this - with rules appropriate to their cluster. + monitoring, ingress, and host-network namespaces, and deny all other ingress traffic. + On Kubernetes, the default allows all ingress traffic, since the namespace of the + cluster's ingress controller is not known to the operator; administrators are expected + to replace this with rules appropriate to their cluster. If this field is specified as an empty list, all ingress traffic to DevWorkspace pods is denied. If this field is specified as a non-empty list, exactly those rules apply and the default rules no longer apply. diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index e6b7126f3..e05101eba 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -4426,10 +4426,10 @@ spec: Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, the defaults allow traffic from the operator's own namespace and from the OpenShift - monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, - the default allows all ingress traffic, since the namespace of the cluster's ingress - controller is not known to the operator; administrators are expected to replace this - with rules appropriate to their cluster. + monitoring, ingress, and host-network namespaces, and deny all other ingress traffic. + On Kubernetes, the default allows all ingress traffic, since the namespace of the + cluster's ingress controller is not known to the operator; administrators are expected + to replace this with rules appropriate to their cluster. If this field is specified as an empty list, all ingress traffic to DevWorkspace pods is denied. If this field is specified as a non-empty list, exactly those rules apply and the default rules no longer apply. diff --git a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index 8c28754fa..388ea2f23 100644 --- a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -4426,10 +4426,10 @@ spec: Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, the defaults allow traffic from the operator's own namespace and from the OpenShift - monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, - the default allows all ingress traffic, since the namespace of the cluster's ingress - controller is not known to the operator; administrators are expected to replace this - with rules appropriate to their cluster. + monitoring, ingress, and host-network namespaces, and deny all other ingress traffic. + On Kubernetes, the default allows all ingress traffic, since the namespace of the + cluster's ingress controller is not known to the operator; administrators are expected + to replace this with rules appropriate to their cluster. If this field is specified as an empty list, all ingress traffic to DevWorkspace pods is denied. If this field is specified as a non-empty list, exactly those rules apply and the default rules no longer apply. diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index ed7d2768d..c8fe99d7e 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -4426,10 +4426,10 @@ spec: Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, the defaults allow traffic from the operator's own namespace and from the OpenShift - monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, - the default allows all ingress traffic, since the namespace of the cluster's ingress - controller is not known to the operator; administrators are expected to replace this - with rules appropriate to their cluster. + monitoring, ingress, and host-network namespaces, and deny all other ingress traffic. + On Kubernetes, the default allows all ingress traffic, since the namespace of the + cluster's ingress controller is not known to the operator; administrators are expected + to replace this with rules appropriate to their cluster. If this field is specified as an empty list, all ingress traffic to DevWorkspace pods is denied. If this field is specified as a non-empty list, exactly those rules apply and the default rules no longer apply. diff --git a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index 8c28754fa..388ea2f23 100644 --- a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -4426,10 +4426,10 @@ spec: Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, the defaults allow traffic from the operator's own namespace and from the OpenShift - monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, - the default allows all ingress traffic, since the namespace of the cluster's ingress - controller is not known to the operator; administrators are expected to replace this - with rules appropriate to their cluster. + monitoring, ingress, and host-network namespaces, and deny all other ingress traffic. + On Kubernetes, the default allows all ingress traffic, since the namespace of the + cluster's ingress controller is not known to the operator; administrators are expected + to replace this with rules appropriate to their cluster. If this field is specified as an empty list, all ingress traffic to DevWorkspace pods is denied. If this field is specified as a non-empty list, exactly those rules apply and the default rules no longer apply. diff --git a/deploy/templates/components/csv/clusterserviceversion.yaml b/deploy/templates/components/csv/clusterserviceversion.yaml index 16b4215eb..419edc792 100644 --- a/deploy/templates/components/csv/clusterserviceversion.yaml +++ b/deploy/templates/components/csv/clusterserviceversion.yaml @@ -1,4 +1,3 @@ ---- apiVersion: operators.coreos.com/v1alpha1 kind: ClusterServiceVersion metadata: diff --git a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml index 96abef38f..728c40ed0 100644 --- a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -4424,10 +4424,10 @@ spec: Ingress defines the ingress rules applied to DevWorkspace pods. If this field is not specified, the default ingress rules of the DevWorkspace Operator apply. On OpenShift, the defaults allow traffic from the operator's own namespace and from the OpenShift - monitoring and ingress namespaces, and deny all other ingress traffic. On Kubernetes, - the default allows all ingress traffic, since the namespace of the cluster's ingress - controller is not known to the operator; administrators are expected to replace this - with rules appropriate to their cluster. + monitoring, ingress, and host-network namespaces, and deny all other ingress traffic. + On Kubernetes, the default allows all ingress traffic, since the namespace of the + cluster's ingress controller is not known to the operator; administrators are expected + to replace this with rules appropriate to their cluster. If this field is specified as an empty list, all ingress traffic to DevWorkspace pods is denied. If this field is specified as a non-empty list, exactly those rules apply and the default rules no longer apply. diff --git a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml index ed4178ecb..025b4c637 100644 --- a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml +++ b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml @@ -1,4 +1,3 @@ ---- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 7e74bf29e..1b772e0ed 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -168,6 +168,11 @@ var ( {NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"network.openshift.io/policy-group": "ingress"}}}, }, }, + { + From: []networkingv1.NetworkPolicyPeer{ + {NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"policy-group.network.openshift.io/host-network": ""}}}, + }, + }, } ) @@ -177,14 +182,6 @@ var ( perWorkspaceStorageSize = resource.MustParse("10Gi") ) -// GetDefaultConfig returns a copy of the operator's default configuration. It has no -// callers inside this repository: it exists for projects that embed DWO as a dependency, -// such as che-operator, which read the defaults in order to extend them rather than -// restate them. -func GetDefaultConfig() *v1alpha1.OperatorConfiguration { - return defaultConfig.DeepCopy() -} - func setDefaultPodSecurityContext() error { if !infrastructure.IsInitialized() { return fmt.Errorf("can not set default pod security context, infrastructure not detected") @@ -222,12 +219,33 @@ func setDefaultOverrideConfig() error { } func setDefaultNetworkPolicy() error { + ingress, egress, err := GetDefaultNetworkPolicy() + if err != nil { + return err + } + + defaultConfig.Workspace.NetworkPolicy = &v1alpha1.NetworkPolicyConfig{ + Enabled: pointer.Bool(constants.DefaultNetworkPolicyEnabled), + Ingress: ingress, + Egress: egress, + } + return nil +} + +// GetDefaultNetworkPolicy returns the default NetworkPolicy applied to DevWorkspace pods. +// It is exposed publicly for other operators (such as che-operator) that need to read +// and extend the default rules rather than hardcoding or duplicating them. +func GetDefaultNetworkPolicy() ( + []networkingv1.NetworkPolicyIngressRule, + []networkingv1.NetworkPolicyEgressRule, + error, +) { if !infrastructure.IsInitialized() { - return fmt.Errorf("can not set default network policy, infrastructure not detected") + return nil, nil, fmt.Errorf("can not set default network policy, infrastructure not detected") } operatorNamespace, err := infrastructure.GetNamespace() if err != nil { - return err + return nil, nil, err } var ingressPolicyRules []networkingv1.NetworkPolicyIngressRule @@ -255,5 +273,5 @@ func setDefaultNetworkPolicy() error { Ingress: ingressPolicyRules, Egress: defaultEgressPolicyRules, } - return nil + return ingressPolicyRules, defaultEgressPolicyRules, nil }