-
Notifications
You must be signed in to change notification settings - Fork 40.9k
KEP-5311 Relaxed validation for Services names #132339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
KEP-5311 Relaxed validation for Services names #132339
Conversation
9cf6d23
to
c8b9edb
Compare
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: adrianmoisey The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
pkg/features/kube_features.go
Outdated
@@ -1641,6 +1647,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate | |||
{Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.37 | |||
}, | |||
|
|||
RelaxedServiceNameValidation: { | |||
{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1.34
@@ -5924,9 +5929,16 @@ var supportedServiceIPFamilyPolicy = sets.New( | |||
core.IPFamilyPolicyRequireDualStack) | |||
|
|||
// ValidateService tests if required fields/annotations of a Service are valid. | |||
func ValidateService(service, oldService *core.Service) field.ErrorList { | |||
func ValidateService(service, oldService *core.Service, isUpdate bool) field.ErrorList { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you don't need isUpdate
; it's an update if oldService != nil
metaPath := field.NewPath("metadata") | ||
allErrs := ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName, metaPath) | ||
|
||
// Avoid validating the name if this is an update, since the name is immutable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Avoid re-validating the name on update, to avoid problems when rolling back RelaxedServiceNameValidation"
AllowInvalidWildcardHostRule: allowInvalidWildcardHostRule(oldIngress), | ||
AllowInvalidSecretName: allowInvalidSecretName(oldIngress), | ||
AllowInvalidWildcardHostRule: allowInvalidWildcardHostRule(oldIngress), | ||
AllowedRelaxedServiceNameValidation: allowRelaxedServiceNameValidation(oldIngress), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just set this always true
for Updates and false
for Creates.
Also, the name is misleading because you obey the feature gate too... I'd make it something like SkipServiceNameValidation
(and then just skip validation)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just set this always true for Updates and false for Creates.
Could you explain the logic behind this? Not sure I understand
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, I forgot there can be multiple Services referred to in the Ingress. So what you have isn't right. The KEP says:
Updates of Ingress will use the previous
NameIsDNS1035Label()
validation for.spec.rules[].http.paths[].backend.service.name
if that field changes, otherwise, there is no validation
But what this implements is "if any service name in oldIngress
requires NameIsDNSLabel
, then validate every service name in ingress
with NameIsDNSLabel
.
To do what the KEP says, you'd have to pass oldIngress
all the way down through the validation... a simpler approach might be to do it like what I did with AllowCIDRsEvenIfInvalid
for NetworkPolicy validation in the same file: at the start of Update validation, make a list of all of the service names in oldIngress
that don't validate with NameIsDNS1035Label
, and then in validateIngressBackend
, allow any of those names to be used in addition to valid names.
This still doesn't implement quite what the KEP says, but it's much closer, and a lot simpler to implement...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given this discussion, I had assumed that what I have implemented in this PR was enough.
I'm happy to change it to be what you've suggested here, that doesn't seem too difficult.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah... hm, ok (though when you update the KEP for beta, you should probably fix the text to match that better)
@@ -15477,6 +15478,7 @@ func TestValidateServiceCreate(t *testing.T) { | |||
name string | |||
tweakSvc func(svc *core.Service) // given a basic valid service, each test case can customize it | |||
numErrs int | |||
featureGates []featuregate.Feature |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably started writing this before I pushed the PreferSameTrafficDistribution update that removed this field, right?
The problem is that having featureGates
works during alpha when the feature is off by default and you're specifying gates that should be enabled, but then it breaks in beta when the feature is on by default. It works better to just have a bool
here for whether to enable this feature gate, like we do with the other two.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably started writing this before I pushed the PreferSameTrafficDistribution update that removed this field, right?
Yup!
The problem is that having
featureGates
works during alpha when the feature is off by default and you're specifying gates that should be enabled
Ah thanks, that makes sense.
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PreferSameTrafficDistribution, tc.newTrafficDist) | ||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, !tc.legacyIPs) | ||
for i := range tc.featureGates { | ||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, tc.featureGates[i], true) | ||
} | ||
svc := makeValidService() | ||
tc.tweakSvc(&svc) | ||
errs := ValidateServiceCreate(&svc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should add a test to ValidateServiceUpdate
too, to ensure that you can update a service with a name that starts with a digit, even if the feature gate is disabled
@@ -1018,6 +1019,7 @@ func TestValidateIngressCreate(t *testing.T) { | |||
testCases := map[string]struct { | |||
tweakIngress func(ingress *networking.Ingress) | |||
expectedErrs field.ErrorList | |||
featureGates []featuregate.Feature |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(same comments as service test)
test/e2e/network/dns.go
Outdated
@@ -654,6 +655,36 @@ var _ = common.SIGDescribe("DNS", func() { | |||
} | |||
validateDNSResults(ctx, f, pod, append(agnhostFileNames, jessieFileNames...)) | |||
}) | |||
|
|||
framework.It("should work with a host that starts with a digit", framework.WithFeatureGate(features.RelaxedServiceNameValidation), func(ctx context.Context) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"should work with a service name that starts with a digit"
test/e2e/network/dns.go
Outdated
fmt.Sprintf("%s.%s.svc.%s", svcName, f.Namespace.Name, framework.TestContext.ClusterDNSDomain), | ||
} | ||
|
||
// TODO: Validate both IPv4 and IPv6 families for dual-stack |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not really needed... there's no reason dual-stack in particular would break in this case
fcd0d82
to
6e0df57
Compare
/retitle KEP-5311 Relaxed validation for Services names |
6e0df57
to
e94834d
Compare
nameFn := ValidateServiceName | ||
if oldService != nil { | ||
nameFn = func(_ string, _ bool) []string { return nil } | ||
} | ||
|
||
allErrs := ValidateObjectMeta(&service.ObjectMeta, true, nameFn, metaPath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we already have ValidateObjectMetaUpdate
that seems much better than creating the noop function
if oldService != nil {
ValidateObjectMetaUpdate
} else {
ValidateObjectMeta
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with a different direction, but put it in its own commit: cf8e372
(#132339)
Let me know what you think
if oldIngress != nil { | ||
for _, rule := range oldIngress.Spec.Rules { | ||
for _, path := range rule.HTTP.Paths { | ||
if path.Backend.Service == nil { | ||
continue | ||
} | ||
serviceName := path.Backend.Service.Name | ||
// If a name doesn't validate with NameIsDNS1035Label, but does validate with NameIsDNSLabel, | ||
// then we allow it to be used as a Service name in an Ingress. | ||
new := apimachineryvalidation.NameIsDNSLabel(serviceName, false) | ||
old := apimachineryvalidation.NameIsDNS1035Label(serviceName, false) | ||
if new == nil && old != nil { | ||
return true | ||
} | ||
} | ||
} | ||
} | ||
return false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, it is a common practice to avoid excessive indentantion, in this case you can just do
if oldIngress == nil {
return false
}
and you get the next section unindented
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
new := apimachineryvalidation.NameIsDNSLabel(serviceName, false) | ||
old := apimachineryvalidation.NameIsDNS1035Label(serviceName, false) | ||
if new == nil && old != nil { | ||
return true | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on slices you should try to avoid checking on nil, since the empty (0 elements) and nil slice has the same value for us here, use len(...) > 0 instead
https://go.dev/play/p/qabz4DFXjh-
Also give better names to the variables newValidationErrors, oldValidationErrors. ... I'm bad at this names but llms helpers use to help with this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good callout, fixed
} | ||
} else { | ||
for _, msg := range apivalidation.ValidateServiceName(backend.Service.Name, false) { | ||
allErrs = append(allErrs, field.Invalid(fldPath.Child("service", "name"), backend.Service.Name, msg)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this now is confusing, since this function is feature gated ... now we are introducing the feature gate logic into this function that IIRC is what we try to avoid by using the IngressValidationOptions
structure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. I'm not too sure of a better path forward. Would it be acceptable to pass a function in via IngressValidationOptions
rather than a boolean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wait, I see what you're saying. The decision needs to be set higher up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦 Ok, I see what you're saying. apivalidation.ValidateServiceName
is gated. Right.
I'm not sure on the best way forward. Some options I can think of:
- Don't gate
apivalidation.ValidateServiceName
and handle the logic another way - Change
validateIngressBackend
to use eitherapimachineryvalidation.NameIsDNSLabel
orapimachineryvalidation.NameIsDNS1035Label
depending on the gate (leaving a comment to revert it back toapivalidation.ValidateServiceName
when the feature is removed) - Some other way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I settled on doing this: https://github.com/kubernetes/kubernetes/compare/3f0292d9d834ff1c008443e793da3d1582519833..ff34de2d9c709dfd7724506ddde46d4a15155e58
Let me know what you think
…sDNSLabel Only validate when feature gate RelaxedServiceNameValidation is enabled. Also remove name validation on Service updates, as the name is immutable.
e94834d
to
3f0292d
Compare
…n.NameIsDNSLabel Only validate when feature gate RelaxedServiceNameValidation is enabled or when the Ingess resource contains a service ref that already validates with apimachineryvalidation.NameIsDNSLabel
Test the behaviour of feature gate RelaxedServiceNameValidation.
Test the behaviour of feature gate RelaxedServiceNameValidation.
To test enabling and disabling of RelaxedServiceNameValidation feature gate
ba9d2c8
to
a13900f
Compare
Put it into ValidateServiceCreate(), making the code path as such: ``` pkg/registry/core/service/strategy.go Validate -> validation.ValidateServiceCreate -> ValidateObjectMeta -> ValidateService ValidateUpdate -> validation.ValidateServiceUpdate -> ValidateObjectMetaUpdate -> ValidateService ``` Other resources I checked pass the update objects through ValidateObjectMeta and ValidateObjectMetaUpdate, so this breaks the pattern, but it seems to be how the ValidateObjectMeta/ValidateObjectMetaUpdate functions are designed to operate.
a13900f
to
cf8e372
Compare
@@ -5926,7 +5931,7 @@ var supportedServiceIPFamilyPolicy = sets.New( | |||
// ValidateService tests if required fields/annotations of a Service are valid. | |||
func ValidateService(service, oldService *core.Service) field.ErrorList { | |||
metaPath := field.NewPath("metadata") | |||
allErrs := ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName, metaPath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leave a comment here to the effect of "Don't validate ObjectMeta here - that is handled in the Create/Update functions which call this."
@@ -5926,7 +5931,7 @@ var supportedServiceIPFamilyPolicy = sets.New( | |||
// ValidateService tests if required fields/annotations of a Service are valid. | |||
func ValidateService(service, oldService *core.Service) field.ErrorList { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe rename to validateService
to ensure no external callers get created?
numErrs: 0, | ||
}, { | ||
name: "invalid: service name begins with a digit feature gate disabled", | ||
tweakSvc: func(s *core.Service) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set relaxedServiceNames: false
so there's no ambiguity?
@@ -19477,6 +19493,14 @@ func TestValidateServiceUpdate(t *testing.T) { | |||
newSvc.Annotations[core.AnnotationLoadBalancerSourceRangesKey] = "010.0.0.0/8, 1.2.3.0/24" | |||
}, | |||
numErrs: 1, | |||
}, { | |||
name: "can modify a pre-existing relaxed service name without error", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this test need to at least assert the gate to false, to really prove it stays working?
if rule.HTTP == nil { | ||
continue | ||
} | ||
for _, path := range rule.HTTP.Paths { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aaron-prindle @yongruilin @jpbetz regarding declarative validation. Paths is "atomic" but a struct. It should PROBABLY have been a list-map but doesn't need multi-owner. I think we eventually need a listType which decouples the managed fields / SSA logic from the ability to do by-key ratcheting. Even the "identical value" ratcheting (which I think we should not do) would not help here -- the update validation would not get an "oldValue" for each Path in Paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though for different purpose, we have added +k8s:
prefix to listType
and have been using it in DV. Switch, rename or reuse the same tagging should be relatively easy when we're ready in the migration process.
@@ -283,6 +285,9 @@ type IngressValidationOptions struct { | |||
|
|||
// AllowInvalidWildcardHostRule indicates whether invalid rule values are allowed in rules with wildcard hostnames | |||
AllowInvalidWildcardHostRule bool | |||
|
|||
// AllowedRelaxedServiceNameValidation indicates if the backend service name can be validated with apimachineryvalidation.NameIsDNSLabel | |||
AllowedRelaxedServiceNameValidation bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AllowedRelax
or AllowRelax...
to align with previous naming?
if rule.HTTP == nil { | ||
continue | ||
} | ||
for _, path := range rule.HTTP.Paths { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though for different purpose, we have added +k8s:
prefix to listType
and have been using it in DV. Switch, rename or reuse the same tagging should be relatively easy when we're ready in the migration process.
What this PR does / why we need it:
Implements the new relaxed variation for Service names
Special notes for your reviewer:
None
Does this PR introduce a user-facing change?
Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:
/kind feature
/sig network
/assign @thockin @aojea @danwinship
/priority important-soon
/triage accepted