Change:
xref I think this may be one of the few cases where we can actually change the behavior in a "breaking" way. User creates service with , is empty Controller or user updates the status subresource, sets (not wiped or validated, should be) User updates in any way, is wiped It's clear that the is not meant to be set when is not "LoadBalancer". The question is: 1) Fail validation? 2) Silently discard the value? 3) Warn and then discard the value? 4) Just warn and leave it. Because we allowed it in the past, but it is so volatile, I lean towards 3. and - the people most familair with this type. Edit: PrepareForUpdate() runs before warnings. So: (1) is easy (2) is easy (4) is slightly less easy than (2) (3) is slightly less easy than (4)
This issue is currently awaiting triage. If a SIG or subproject determines this is a relevant issue, they will accept it by applying the label and provide further guidance. The label can be by org members by writing in a comment. <detailsInstructions for interacting with me using PR comments are available . If you have questions or suggestions related to my behavior, please file an issue against the repository. </details
OMG change it to clusterIP as expected Status is wiped update status as ClusterIP ... and if I edit the service back to loadbalancer it keeps the status??
/kind bug
/retitle Service can be set when !=
We talked about this one at the SIG Network meeting today. suggested we use the feature gate for deprecation option to phase the current (problematic) functionality out.
/triage accept
The label(s) cannot be applied, because the repository doesn't have them. <detailsIn response to : /triage accept Instructions for interacting with me using PR comments are available . If you have questions or suggestions related to my behavior, please file an issue against the repository. </details

Discussion:
// ValidateServiceStatusUpdate tests if required fields in the Service are set when updating status. func ValidateServiceStatusUpdate(service, oldService *core.Service) field.ErrorList { allErrs := ValidateObjectMetaUpdate(&service.ObjectMeta, &oldService.ObjectMeta, field.NewPath("metadata")) <del> allErrs = append(allErrs, ValidateLoadBalancerStatus(&service.Status.LoadBalancer, field.NewPath("status", "loadBalancer"))...) </del> <ins> allErrs = append(allErrs, ValidateLoadBalancerStatus(&service.Status.LoadBalancer, field.NewPath("status", "loadBalancer"), &service.Spec)...) </ins> return allErrs }

) // ValidateLoadBalancerStatus validates required fields on a LoadBalancerStatus <del> func ValidateLoadBalancerStatus(status *core.LoadBalancerStatus, fldPath *field.Path) field.ErrorList { </del> <ins> func ValidateLoadBalancerStatus(status *core.LoadBalancerStatus, fldPath *field.Path, spec *core.ServiceSpec) field.ErrorList { </ins> allErrs := field.ErrorList{} <del> for i, ingress := range status.Ingress { idxPath := fldPath.Child("ingress").Index(i) if len(ingress.IP) > 0 { if isIP := (netutils.ParseIPSloppy(ingress.IP) != nil); !isIP { allErrs = append(allErrs, field.Invalid(idxPath.Child("ip"), ingress.IP, "must be a valid IP address")) } } if utilfeature.DefaultFeatureGate.Enabled(features.LoadBalancerIPMode) && ingress.IPMode == nil { </del> <ins> ingrPath := fldPath.Child("ingress") if !utilfeature.DefaultFeatureGate.Enabled(features.AllowServiceLBStatusOnNonLB) && spec.Type != core.ServiceTypeLoadBalancer && len(status.Ingress) != 0 { allErrs = append(allErrs, field.Forbidden(ingrPath, "may only be used when `spec.type` is 'LoadBalancer'")) } else { for i, ingress := range status.Ingress { idxPath := ingrPath.Index(i) </ins> if len(ingress.IP) > 0 { <del> allErrs = append(allErrs, field.Required(idxPath.Child("ipMode"), "must be specified when `ip` is set")) </del> <ins> if isIP := (netutils.ParseIPSloppy(ingress.IP) != nil); !isIP { allErrs = append(allErrs, field.Invalid(idxPath.Child("ip"), ingress.IP, "must be a valid IP address")) } </ins> } <del> } else if ingress.IPMode != nil && len(ingress.IP) == 0 { allErrs = append(allErrs, field.Forbidden(idxPath.Child("ipMode"), "may not be specified when `ip` is not set")) } else if ingress.IPMode != nil && !supportedLoadBalancerIPMode.Has(string(*ingress.IPMode)) { allErrs = append(allErrs, field.NotSupported(idxPath.Child("ipMode"), ingress.IPMode, supportedLoadBalancerIPMode.List())) } if len(ingress.Hostname) > 0 { for _, msg := range validation.IsDNS1123Subdomain(ingress.Hostname) { allErrs = append(allErrs, field.Invalid(idxPath.Child("hostname"), ingress.Hostname, msg)) </del> <ins> if utilfeature.DefaultFeatureGate.Enabled(features.LoadBalancerIPMode) && ingress.IPMode == nil { if len(ingress.IP) > 0 { allErrs = append(allErrs, field.Required(idxPath.Child("ipMode"), "must be specified when `ip` is set")) } } else if ingress.IPMode != nil && len(ingress.IP) == 0 { allErrs = append(allErrs, field.Forbidden(idxPath.Child("ipMode"), "may not be specified when `ip` is not set")) } else if ingress.IPMode != nil && !supportedLoadBalancerIPMode.Has(string(*ingress.IPMode)) { allErrs = append(allErrs, field.NotSupported(idxPath.Child("ipMode"), ingress.IPMode, supportedLoadBalancerIPMode.List())) </ins> } <del> if isIP := (netutils.ParseIPSloppy(ingress.Hostname) != nil); isIP { allErrs = append(allErrs, field.Invalid(idxPath.Child("hostname"), ingress.Hostname, "must be a DNS name, not an IP address")) </del> <ins> if len(ingress.Hostname) > 0 { for _, msg := range validation.IsDNS1123Subdomain(ingress.Hostname) { allErrs = append(allErrs, field.Invalid(idxPath.Child("hostname"), ingress.Hostname, msg)) } if isIP := (netutils.ParseIPSloppy(ingress.Hostname) != nil); isIP { allErrs = append(allErrs, field.Invalid(idxPath.Child("hostname"), ingress.Hostname, "must be a DNS name, not an IP address")) } </ins> } } }

testCases := []struct { name          string ipModeEnabled bool <ins> nonLBAllowed  bool </ins> tweakLBStatus func(s *core.LoadBalancerStatus) <ins> tweakSvcSpec  func(s *core.ServiceSpec) </ins> numErrs       int }{ <del> /* LoadBalancerIPMode*/ </del> { <ins> name:         "type is not LB", nonLBAllowed: false, tweakSvcSpec: func(s *core.ServiceSpec) { s.Type = core.ServiceTypeClusterIP }, tweakLBStatus: func(s *core.LoadBalancerStatus) { s.Ingress = []core.LoadBalancerIngress{{ IP: "1.2.3.4", }} }, numErrs: 1, }, { name:         "type is not LB. back-compat", nonLBAllowed: true, tweakSvcSpec: func(s *core.ServiceSpec) { s.Type = core.ServiceTypeClusterIP }, tweakLBStatus: func(s *core.LoadBalancerStatus) { s.Ingress = []core.LoadBalancerIngress{{ IP: "1.2.3.4", }} }, numErrs: 0, }, { </ins> name:          "valid vip ipMode", ipModeEnabled: true, tweakLBStatus: func(s *core.LoadBalancerStatus) {

for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LoadBalancerIPMode, tc.ipModeEnabled)() <del> s := core.LoadBalancerStatus{} tc.tweakLBStatus(&s) errs := ValidateLoadBalancerStatus(&s, field.NewPath("status")) </del> <ins> defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AllowServiceLBStatusOnNonLB, tc.nonLBAllowed)() status := core.LoadBalancerStatus{} tc.tweakLBStatus(&status) spec := core.ServiceSpec{Type: core.ServiceTypeLoadBalancer} if tc.tweakSvcSpec != nil { tc.tweakSvcSpec(&spec) } errs := ValidateLoadBalancerStatus(&status, field.NewPath("status"), &spec) </ins> if len(errs) != tc.numErrs { t.Errorf("Unexpected error list for case %q(expected:%v got %v) - Errors:n %v", tc.name, tc.numErrs, len(errs), errs.ToAggregate()) }

// Enable usage of Provision of PVCs from snapshots in other namespaces CrossNamespaceVolumeDataSource featuregate.Feature = "CrossNamespaceVolumeDataSource" <ins> // owner: @thockin // deprecated: v1.29 // // Enables Service.status.ingress.loadBanace to be set on // services of types other than LoadBalancer. AllowServiceLBStatusOnNonLB featuregate.Feature = "AllowServiceLBStatusOnNonLB" </ins> // owner: @bswartz // alpha: v1.18 // beta: v1.24

var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ CrossNamespaceVolumeDataSource: {Default: false, PreRelease: featuregate.Alpha}, <ins> AllowServiceLBStatusOnNonLB: {Default: false, PreRelease: featuregate.Deprecated}, // remove after 1.29 </ins> AnyVolumeDataSource: {Default: true, PreRelease: featuregate.Beta}, // on by default in 1.24 APISelfSubjectReview: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // GA in 1.28; remove in 1.30

t.Errorf("Service must be namespace scoped") } oldService := makeValidService() <del> newService := makeValidService() </del> <ins> oldService.Spec.Type = api.ServiceTypeLoadBalancer </ins> oldService.ResourceVersion = "4" <del> newService.ResourceVersion = "4" </del> <ins> oldService.Spec.SessionAffinity = "None" newService := oldService.DeepCopy() </ins> newService.Spec.SessionAffinity = "ClientIP" newService.Status = api.ServiceStatus{ LoadBalancer: api.LoadBalancerStatus{

gvr("", "v1", "pods"):                                                          `{"metadata": {"deletionTimestamp": "2020-01-01T00:00:00Z", "ownerReferences":[]}, "spec": {"containers": [{"image": "` + image2 + `", "name": "container7"}]}}`, gvr("", "v1", "replicationcontrollers"):                                        `{"spec": {"selector": {"new": "stuff2"}}}`, gvr("", "v1", "resourcequotas"):                                                `{"spec": {"hard": {"cpu": "25M"}}}`, <del> gvr("", "v1", "services"):                                                      `{"spec": {"externalName": "service2name"}}`, </del> <ins> gvr("", "v1", "services"):                                                      `{"spec": {"type": "ClusterIP"}}`, </ins> gvr("apps", "v1", "daemonsets"):                                                `{"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container6"}]}}}}`, gvr("apps", "v1", "deployments"):                                               `{"metadata": {"labels": {"a":"c"}}, "spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container6"}]}}}}`, gvr("apps", "v1", "replicasets"):                                               `{"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container4"}]}}}}`,

ExpectedEtcdPath: "/registry/configmaps/" + namespace + "/cm1", }, gvr("", "v1", "services"): { <del> Stub:             `{"metadata": {"name": "service1"}, "spec": {"externalName": "service1name", "ports": [{"port": 10000, "targetPort": 11000}], "selector": {"test": "data"}}}`, </del> <ins> Stub:             `{"metadata": {"name": "service1"}, "spec": {"type": "LoadBalancer", "ports": [{"port": 10000, "targetPort": 11000}], "selector": {"test": "data"}}}`, </ins> ExpectedEtcdPath: "/registry/services/specs/" + namespace + "/service1", }, gvr("", "v1", "podtemplates"): {