From ac40d355e65c49e0a17854e0f73a515fa784f5ba Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Fri, 18 Sep 2026 12:25:42 +0200 Subject: [PATCH 1/3] 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/3] 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/3] 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"]