diff --git a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go index 0aeba956c..890300213 100644 --- a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go +++ b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go @@ -351,14 +351,14 @@ type NetworkPolicyConfig struct { // 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"` + Ingress []networkingv1.NetworkPolicyIngressRule `json:"ingress"` // 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"` + Egress []networkingv1.NetworkPolicyEgressRule `json:"egress"` } type Proxy struct { diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 5ce3710bd..7a071892c 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -269,10 +269,28 @@ func GetDefaultNetworkPolicy(operatorNamespace string) ( ingressPolicyRules = defaultKubernetesIngressPolicyRules } - defaultConfig.Workspace.NetworkPolicy = &v1alpha1.NetworkPolicyConfig{ - Enabled: pointer.Bool(constants.DefaultNetworkPolicyEnabled), - Ingress: ingressPolicyRules, - Egress: defaultEgressPolicyRules, + // Return copies so callers can extend the rules without mutating package-level defaults + return deepCopyIngressRules(ingressPolicyRules), deepCopyEgressRules(defaultEgressPolicyRules), nil +} + +func deepCopyIngressRules(rules []networkingv1.NetworkPolicyIngressRule) []networkingv1.NetworkPolicyIngressRule { + if rules == nil { + return nil + } + copied := make([]networkingv1.NetworkPolicyIngressRule, len(rules)) + for i := range rules { + rules[i].DeepCopyInto(&copied[i]) + } + return copied +} + +func deepCopyEgressRules(rules []networkingv1.NetworkPolicyEgressRule) []networkingv1.NetworkPolicyEgressRule { + if rules == nil { + return nil + } + copied := make([]networkingv1.NetworkPolicyEgressRule, len(rules)) + for i := range rules { + rules[i].DeepCopyInto(&copied[i]) } - return ingressPolicyRules, defaultEgressPolicyRules, nil + return copied } diff --git a/pkg/config/sync_test.go b/pkg/config/sync_test.go index 05199936a..6fb85e53f 100644 --- a/pkg/config/sync_test.go +++ b/pkg/config/sync_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/assert" 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/apimachinery/pkg/runtime" @@ -615,6 +616,71 @@ func TestMergeConfigMergesRuntimeClassName(t *testing.T) { } } +func TestResolveConfigForWorkspaceWithNetworkPolicy(t *testing.T) { + setupForTest(t) + + workspace := &dw.DevWorkspace{ + Spec: dw.DevWorkspaceSpec{ + Template: dw.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: dw.DevWorkspaceTemplateSpecContent{ + Attributes: attributes.Attributes{}, + }, + }, + }, + } + + attrs := workspace.Spec.Template.DevWorkspaceTemplateSpecContent.Attributes + attrs.Put( + constants.ExternalDevWorkspaceConfiguration, + types.NamespacedName{ + Name: externalConfigName, + Namespace: externalConfigNamespace, + }, + nil, + ) + + t.Run("external config overrides NetworkPolicy with empty ingress and egress (deny all)", func(t *testing.T) { + externalDwoc := &v1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: externalConfigName, + Namespace: externalConfigNamespace, + }, + Config: &v1alpha1.OperatorConfiguration{ + Workspace: &v1alpha1.WorkspaceConfig{ + NetworkPolicy: &v1alpha1.NetworkPolicyConfig{ + Enabled: pointer.Bool(true), + Ingress: []networkingv1.NetworkPolicyIngressRule{}, + Egress: []networkingv1.NetworkPolicyEgressRule{}, + }, + }, + }, + } + + dwoc := &v1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: OperatorConfigName, + Namespace: configNamespace, + }, + Config: defaultConfig.DeepCopy(), + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(dwoc, externalDwoc).Build() + err := SetupControllerConfig(client) + assert.NoError(t, err) + + resolvedConfig, err := ResolveConfigForWorkspace(workspace, client) + assert.NoError(t, err) + assert.NotNil(t, resolvedConfig.Workspace.NetworkPolicy) + assert.Equal(t, pointer.Bool(true), resolvedConfig.Workspace.NetworkPolicy.Enabled) + + assert.NotNil(t, resolvedConfig.Workspace.NetworkPolicy.Ingress) + assert.Empty(t, resolvedConfig.Workspace.NetworkPolicy.Ingress) + + assert.NotNil(t, resolvedConfig.Workspace.NetworkPolicy.Egress) + assert.Empty(t, resolvedConfig.Workspace.NetworkPolicy.Egress) + }) +} + func fuzzQuantity(q *resource.Quantity, c fuzz.Continue) { q.Set(c.Int63n(999)) q.Format = resource.DecimalSI