Code change:
return nil } <ins> func TestUpgradeServicePreferToDualStack(t *testing.T) { // Create an IPv4 only dual stack control-plane serviceCIDR := "192.168.0.0/24" defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, true)() cfg := framework.NewIntegrationTestControlPlaneConfig() _, cidr, err := net.ParseCIDR(serviceCIDR) if err != nil { t.Fatalf("bad cidr: %v", err) } cfg.ExtraConfig.ServiceIPRange = *cidr _, s, closeFn := framework.RunAnAPIServer(cfg) client := clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL}) // Wait until the default "kubernetes" service is created. if err = wait.Poll(250*time.Millisecond, time.Minute, func() (bool, error) { _, err := client.CoreV1().Services(metav1.NamespaceDefault).Get(context.TODO(), "kubernetes", metav1.GetOptions{}) if err != nil && !apierrors.IsNotFound(err) { return false, err } return !apierrors.IsNotFound(err), nil }); err != nil { t.Fatalf("creating kubernetes service timed out") } preferDualStack := v1.IPFamilyPolicyPreferDualStack svc := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc-prefer-dual", }, Spec: v1.ServiceSpec{ Type:           v1.ServiceTypeClusterIP, ClusterIPs:     nil, IPFamilies:     nil, IPFamilyPolicy: &preferDualStack, Ports: []v1.ServicePort{ { Name:       "svc-port-1", Port:       443, TargetPort: intstr.IntOrString{IntVal: 443}, Protocol:   "TCP", }, }, }, } // create the service _, err = client.CoreV1().Services(metav1.NamespaceDefault).Create(context.TODO(), svc, metav1.CreateOptions{}) if err != nil { t.Fatalf("Unexpected error: %v", err) } // validate the service was created correctly if it was not expected to fail svc, err = client.CoreV1().Services(metav1.NamespaceDefault).Get(context.TODO(), svc.Name, metav1.GetOptions{}) if err != nil { t.Fatalf("Unexpected error to get the service %s %v", svc.Name, err) } if err := validateServiceAndClusterIPFamily(svc, []v1.IPFamily{v1.IPv4Protocol}); err != nil { t.Fatalf("Unexpected error validating the service %s %v", svc.Name, err) } // reconfigure the apiserver to be dual-stack closeFn() secondaryServiceCIDR := "2001:db8:1::/48" _, secCidr, err := net.ParseCIDR(secondaryServiceCIDR) if err != nil { t.Fatalf("bad cidr: %v", err) } cfg.ExtraConfig.SecondaryServiceIPRange = *secCidr _, s, closeFn = framework.RunAnAPIServer(cfg) defer closeFn() client = clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL}) // Wait until the default "kubernetes" service is created. if err = wait.Poll(250*time.Millisecond, time.Minute, func() (bool, error) { _, err := client.CoreV1().Services(metav1.NamespaceDefault).Get(context.TODO(), "kubernetes", metav1.GetOptions{}) if err != nil && !apierrors.IsNotFound(err) { return false, err } return !apierrors.IsNotFound(err), nil }); err != nil { t.Fatalf("creating kubernetes service timed out") } // validate the service was created correctly if it was not expected to fail svc, err = client.CoreV1().Services(metav1.NamespaceDefault).Get(context.TODO(), svc.Name, metav1.GetOptions{}) if err != nil { t.Fatalf("Unexpected error to get the service %s %v", svc.Name, err) } // service should remain single stack if err = validateServiceAndClusterIPFamily(svc, []v1.IPFamily{v1.IPv4Protocol}); err != nil { t.Fatalf("Unexpected error validating the service %s %v", svc.Name, err) } } func TestDowngradeServicePreferToDualStack(t *testing.T) { // Create a dual stack control-plane serviceCIDR := "192.168.0.0/24" secondaryServiceCIDR := "2001:db8:1::/48" defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, true)() dualStackCfg := framework.NewIntegrationTestControlPlaneConfig() _, cidr, err := net.ParseCIDR(serviceCIDR) if err != nil { t.Fatalf("bad cidr: %v", err) } dualStackCfg.ExtraConfig.ServiceIPRange = *cidr _, secCidr, err := net.ParseCIDR(secondaryServiceCIDR) if err != nil { t.Fatalf("bad cidr: %v", err) } dualStackCfg.ExtraConfig.SecondaryServiceIPRange = *secCidr _, s, closeFn := framework.RunAnAPIServer(dualStackCfg) client := clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL}) // Wait until the default "kubernetes" service is created. if err = wait.Poll(250*time.Millisecond, time.Minute, func() (bool, error) { _, err := client.CoreV1().Services(metav1.NamespaceDefault).Get(context.TODO(), "kubernetes", metav1.GetOptions{}) if err != nil && !apierrors.IsNotFound(err) { return false, err } return !apierrors.IsNotFound(err), nil }); err != nil { t.Fatalf("creating kubernetes service timed out") } preferDualStack := v1.IPFamilyPolicyPreferDualStack svc := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc-prefer-dual01", }, Spec: v1.ServiceSpec{ Type:           v1.ServiceTypeClusterIP, ClusterIPs:     nil, IPFamilies:     nil, IPFamilyPolicy: &preferDualStack, Ports: []v1.ServicePort{ { Name:       "svc-port-1", Port:       443, TargetPort: intstr.IntOrString{IntVal: 443}, Protocol:   "TCP", }, }, }, } // create the service _, err = client.CoreV1().Services(metav1.NamespaceDefault).Create(context.TODO(), svc, metav1.CreateOptions{}) if err != nil { t.Fatalf("Unexpected error: %v", err) } // validate the service was created correctly if it was not expected to fail svc, err = client.CoreV1().Services(metav1.NamespaceDefault).Get(context.TODO(), svc.Name, metav1.GetOptions{}) if err != nil { t.Fatalf("Unexpected error to get the service %s %v", svc.Name, err) } if err := validateServiceAndClusterIPFamily(svc, []v1.IPFamily{v1.IPv4Protocol, v1.IPv6Protocol}); err != nil { t.Fatalf("Unexpected error validating the service %s %v", svc.Name, err) } // reconfigure the apiserver to be sinlge stack closeFn() // reset secondary var emptyCidr net.IPNet dualStackCfg.ExtraConfig.SecondaryServiceIPRange = emptyCidr _, s, closeFn = framework.RunAnAPIServer(dualStackCfg) defer closeFn() client = clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL}) // Wait until the default "kubernetes" service is created. if err = wait.Poll(250*time.Millisecond, time.Minute, func() (bool, error) { _, err := client.CoreV1().Services(metav1.NamespaceDefault).Get(context.TODO(), "kubernetes", metav1.GetOptions{}) if err != nil && !apierrors.IsNotFound(err) { return false, err } return !apierrors.IsNotFound(err), nil }); err != nil { t.Fatalf("creating kubernetes service timed out") } // validate the service is still there. svc, err = client.CoreV1().Services(metav1.NamespaceDefault).Get(context.TODO(), svc.Name, metav1.GetOptions{}) if err != nil { t.Fatalf("Unexpected error to get the service %s %v", svc.Name, err) } // service should be single stack if err = validateServiceAndClusterIPFamily(svc, []v1.IPFamily{v1.IPv4Protocol, v1.IPv6Protocol}); err != nil { t.Fatalf("Unexpected error validating the service %s %v", svc.Name, err) } } </ins>

Related discussion:
text: 1) start with a single stack cluster 2) create a PreferDualStack service 3) configure API server with a 2nd service CIDR 4) Confirm the above service is not dual-stack 5) Add a label to the above service 6) Observe that the service becomes dual-stack I think the correct answer is to have a "repair loop" that upgrades (and downgrades!) PreferDualStack Services to match the config. I think this is a GA blocker.
/triage accepted The current thought is: of type with are special new breed of api objects that I don't think we have any similar precedent in the apis, i could be wrong. Their behavior is tied to how the cluster is configured. Thus their behavior must change as the cluster behavior change. a way the user is stating . If it is cluster then i am good with two ClusterIPs. If it is single stack cluster then I am also good with a single ClusterIP. One can assume that if the user was ok was with getting a additional IP when possible then they must be ok with taking it away when it is no longer possible. Based on the above I believe best course of action is create a new repair loop or piggyback on any of the existing (or better: fix it in rest stack during list and get calls). The new logic takes care of matching to the .
reproducer and regression test if somebody takes this, feel free to add it
ack - Scheduled to tackle 3rd week of May. Will definitely include that patch.