diff --git a/config/core/configmaps/features.yaml b/config/core/configmaps/features.yaml index 7e7677556986..7a4c3839bb67 100644 --- a/config/core/configmaps/features.yaml +++ b/config/core/configmaps/features.yaml @@ -23,7 +23,7 @@ metadata: app.kubernetes.io/version: devel serving.knative.dev/release: devel annotations: - knative.dev/example-checksum: "d9e300ba" + knative.dev/example-checksum: "e1c6e542" data: _example: |- ################################ @@ -53,6 +53,12 @@ data: # See: https://knative.dev/docs/serving/feature-flags/#kubernetes-node-affinity kubernetes.podspec-affinity: "disabled" + # Indicates whether Kubernetes topologySpreadConstraints support is enabled + # + # WARNING: Cannot safely be disabled once enabled. + # See: https://knative.dev/docs/serving/feature-flags/#kubernetes-topology-spread-constraints + kubernetes.podspec-topologyspreadconstraints: "disabled" + # Indicates whether Kubernetes hostAliases support is enabled # # WARNING: Cannot safely be disabled once enabled. diff --git a/pkg/apis/config/features.go b/pkg/apis/config/features.go index 0998025e3898..f1339bfa0083 100644 --- a/pkg/apis/config/features.go +++ b/pkg/apis/config/features.go @@ -41,24 +41,25 @@ const ( func defaultFeaturesConfig() *Features { return &Features{ - MultiContainer: Enabled, - PodSpecAffinity: Disabled, - PodSpecDryRun: Allowed, - PodSpecHostAliases: Disabled, - PodSpecFieldRef: Disabled, - PodSpecNodeSelector: Disabled, - PodSpecRuntimeClassName: Disabled, - PodSpecSecurityContext: Disabled, - PodSpecPriorityClassName: Disabled, - PodSpecSchedulerName: Disabled, - ContainerSpecAddCapabilities: Disabled, - PodSpecTolerations: Disabled, - PodSpecVolumesEmptyDir: Disabled, - PodSpecPersistentVolumeClaim: Disabled, - PodSpecPersistentVolumeWrite: Disabled, - PodSpecInitContainers: Disabled, - TagHeaderBasedRouting: Disabled, - AutoDetectHTTP2: Disabled, + MultiContainer: Enabled, + PodSpecAffinity: Disabled, + PodSpecTopologySpreadConstraints: Disabled, + PodSpecDryRun: Allowed, + PodSpecHostAliases: Disabled, + PodSpecFieldRef: Disabled, + PodSpecNodeSelector: Disabled, + PodSpecRuntimeClassName: Disabled, + PodSpecSecurityContext: Disabled, + PodSpecPriorityClassName: Disabled, + PodSpecSchedulerName: Disabled, + ContainerSpecAddCapabilities: Disabled, + PodSpecTolerations: Disabled, + PodSpecVolumesEmptyDir: Disabled, + PodSpecPersistentVolumeClaim: Disabled, + PodSpecPersistentVolumeWrite: Disabled, + PodSpecInitContainers: Disabled, + TagHeaderBasedRouting: Disabled, + AutoDetectHTTP2: Disabled, } } @@ -69,6 +70,7 @@ func NewFeaturesConfigFromMap(data map[string]string) (*Features, error) { if err := cm.Parse(data, asFlag("multi-container", &nc.MultiContainer), asFlag("kubernetes.podspec-affinity", &nc.PodSpecAffinity), + asFlag("kubernetes.podspec-topologyspreadconstraints", &nc.PodSpecTopologySpreadConstraints), asFlag("kubernetes.podspec-dryrun", &nc.PodSpecDryRun), asFlag("kubernetes.podspec-hostaliases", &nc.PodSpecHostAliases), asFlag("kubernetes.podspec-fieldref", &nc.PodSpecFieldRef), @@ -97,24 +99,25 @@ func NewFeaturesConfigFromConfigMap(config *corev1.ConfigMap) (*Features, error) // Features specifies which features are allowed by the webhook. type Features struct { - MultiContainer Flag - PodSpecAffinity Flag - PodSpecDryRun Flag - PodSpecFieldRef Flag - PodSpecHostAliases Flag - PodSpecNodeSelector Flag - PodSpecRuntimeClassName Flag - PodSpecSecurityContext Flag - PodSpecPriorityClassName Flag - PodSpecSchedulerName Flag - ContainerSpecAddCapabilities Flag - PodSpecTolerations Flag - PodSpecVolumesEmptyDir Flag - PodSpecInitContainers Flag - PodSpecPersistentVolumeClaim Flag - PodSpecPersistentVolumeWrite Flag - TagHeaderBasedRouting Flag - AutoDetectHTTP2 Flag + MultiContainer Flag + PodSpecAffinity Flag + PodSpecTopologySpreadConstraints Flag + PodSpecDryRun Flag + PodSpecFieldRef Flag + PodSpecHostAliases Flag + PodSpecNodeSelector Flag + PodSpecRuntimeClassName Flag + PodSpecSecurityContext Flag + PodSpecPriorityClassName Flag + PodSpecSchedulerName Flag + ContainerSpecAddCapabilities Flag + PodSpecTolerations Flag + PodSpecVolumesEmptyDir Flag + PodSpecInitContainers Flag + PodSpecPersistentVolumeClaim Flag + PodSpecPersistentVolumeWrite Flag + TagHeaderBasedRouting Flag + AutoDetectHTTP2 Flag } // asFlag parses the value at key as a Flag into the target, if it exists. diff --git a/pkg/apis/config/features_test.go b/pkg/apis/config/features_test.go index 16dcee7d584c..10592e92b873 100644 --- a/pkg/apis/config/features_test.go +++ b/pkg/apis/config/features_test.go @@ -59,30 +59,32 @@ func TestFeaturesConfiguration(t *testing.T) { name: "features Enabled", wantErr: false, wantFeatures: defaultWith(&Features{ - MultiContainer: Enabled, - PodSpecAffinity: Enabled, - PodSpecDryRun: Enabled, - PodSpecHostAliases: Enabled, - PodSpecNodeSelector: Enabled, - PodSpecRuntimeClassName: Enabled, - PodSpecSecurityContext: Enabled, - PodSpecTolerations: Enabled, - PodSpecPriorityClassName: Enabled, - PodSpecSchedulerName: Enabled, - TagHeaderBasedRouting: Enabled, - }), - data: map[string]string{ - "multi-container": "Enabled", - "kubernetes.podspec-affinity": "Enabled", - "kubernetes.podspec-dryrun": "Enabled", - "kubernetes.podspec-hostaliases": "Enabled", - "kubernetes.podspec-nodeselector": "Enabled", - "kubernetes.podspec-runtimeclassname": "Enabled", - "kubernetes.podspec-securitycontext": "Enabled", - "kubernetes.podspec-tolerations": "Enabled", - "kubernetes.podspec-priorityclassname": "Enabled", - "kubernetes.podspec-schedulername": "Enabled", - "tag-header-based-routing": "Enabled", + MultiContainer: Enabled, + PodSpecAffinity: Enabled, + PodSpecTopologySpreadConstraints: Enabled, + PodSpecDryRun: Enabled, + PodSpecHostAliases: Enabled, + PodSpecNodeSelector: Enabled, + PodSpecRuntimeClassName: Enabled, + PodSpecSecurityContext: Enabled, + PodSpecTolerations: Enabled, + PodSpecPriorityClassName: Enabled, + PodSpecSchedulerName: Enabled, + TagHeaderBasedRouting: Enabled, + }), + data: map[string]string{ + "multi-container": "Enabled", + "kubernetes.podspec-affinity": "Enabled", + "kubernetes.podspec-topologyspreadconstraints": "Enabled", + "kubernetes.podspec-dryrun": "Enabled", + "kubernetes.podspec-hostaliases": "Enabled", + "kubernetes.podspec-nodeselector": "Enabled", + "kubernetes.podspec-runtimeclassname": "Enabled", + "kubernetes.podspec-securitycontext": "Enabled", + "kubernetes.podspec-tolerations": "Enabled", + "kubernetes.podspec-priorityclassname": "Enabled", + "kubernetes.podspec-schedulername": "Enabled", + "tag-header-based-routing": "Enabled", }, }, { name: "multi-container Allowed", @@ -129,6 +131,33 @@ func TestFeaturesConfiguration(t *testing.T) { data: map[string]string{ "kubernetes.podspec-affinity": "Disabled", }, + }, { + name: "kubernetes.podspec-topologyspreadconstraints Allowed", + wantErr: false, + wantFeatures: defaultWith(&Features{ + PodSpecTopologySpreadConstraints: Allowed, + }), + data: map[string]string{ + "kubernetes.podspec-topologyspreadconstraints": "Allowed", + }, + }, { + name: "kubernetes.podspec-topologyspreadconstraints Enabled", + wantErr: false, + wantFeatures: defaultWith(&Features{ + PodSpecTopologySpreadConstraints: Enabled, + }), + data: map[string]string{ + "kubernetes.podspec-topologyspreadconstraints": "Enabled", + }, + }, { + name: "kubernetes.podspec-topologyspreadconstraints Disabled", + wantErr: false, + wantFeatures: defaultWith(&Features{ + PodSpecTopologySpreadConstraints: Disabled, + }), + data: map[string]string{ + "kubernetes.podspec-topologyspreadconstraints": "Disabled", + }, }, { name: "kubernetes.podspec-fieldref Allowed", wantErr: false, diff --git a/pkg/apis/serving/fieldmask.go b/pkg/apis/serving/fieldmask.go index 32723a1f8586..0ea02706a149 100644 --- a/pkg/apis/serving/fieldmask.go +++ b/pkg/apis/serving/fieldmask.go @@ -200,6 +200,9 @@ func PodSpecMask(ctx context.Context, in *corev1.PodSpec) *corev1.PodSpec { if cfg.Features.PodSpecAffinity != config.Disabled { out.Affinity = in.Affinity } + if cfg.Features.PodSpecTopologySpreadConstraints != config.Disabled { + out.TopologySpreadConstraints = in.TopologySpreadConstraints + } if cfg.Features.PodSpecHostAliases != config.Disabled { out.HostAliases = in.HostAliases } diff --git a/pkg/apis/serving/k8s_validation_test.go b/pkg/apis/serving/k8s_validation_test.go index 3d22a4de9050..bf1686a7f325 100644 --- a/pkg/apis/serving/k8s_validation_test.go +++ b/pkg/apis/serving/k8s_validation_test.go @@ -26,6 +26,7 @@ import ( "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/validation" "knative.dev/pkg/apis" @@ -65,6 +66,13 @@ func withPodSpecAffinityEnabled() configOption { } } +func withPodSpecTopologySpreadConstraintsEnabled() configOption { + return func(cfg *config.Config) *config.Config { + cfg.Features.PodSpecTopologySpreadConstraints = config.Enabled + return cfg + } +} + func withPodSpecHostAliasesEnabled() configOption { return func(cfg *config.Config) *config.Config { cfg.Features.PodSpecHostAliases = config.Enabled @@ -1033,6 +1041,27 @@ func TestPodSpecFeatureValidation(t *testing.T) { Paths: []string{"affinity"}, }, cfgOpts: []configOption{withPodSpecAffinityEnabled()}, + }, { + name: "TopologySpreadConstraints", + featureSpec: corev1.PodSpec{ + TopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ + MaxSkew: 1, + TopologyKey: "topology.kubernetes.io/zone", + WhenUnsatisfiable: "DoNotSchedule", + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{ + Key: "key", + Operator: "In", + Values: []string{"value"}, + }}, + }, + }}, + }, + err: &apis.FieldError{ + Message: "must not set the field(s)", + Paths: []string{"topologySpreadConstraints"}, + }, + cfgOpts: []configOption{withPodSpecTopologySpreadConstraintsEnabled()}, }, { name: "HostAliases", featureSpec: corev1.PodSpec{