diff --git a/staging/src/k8s.io/cloud-provider/go.sum b/staging/src/k8s.io/cloud-provider/go.sum index e98a8fe702e..e16677cd9a8 100644 --- a/staging/src/k8s.io/cloud-provider/go.sum +++ b/staging/src/k8s.io/cloud-provider/go.sum @@ -21,6 +21,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= diff --git a/staging/src/k8s.io/cloud-provider/service/helpers/BUILD b/staging/src/k8s.io/cloud-provider/service/helpers/BUILD index 0761000dcca..793b6d39f9f 100644 --- a/staging/src/k8s.io/cloud-provider/service/helpers/BUILD +++ b/staging/src/k8s.io/cloud-provider/service/helpers/BUILD @@ -8,6 +8,9 @@ go_library( visibility = ["//visibility:public"], deps = [ "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library", + "//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library", "//vendor/k8s.io/utils/net:go_default_library", ], ) @@ -33,6 +36,7 @@ go_test( deps = [ "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library", "//vendor/k8s.io/utils/net:go_default_library", ], ) diff --git a/staging/src/k8s.io/cloud-provider/service/helpers/helper.go b/staging/src/k8s.io/cloud-provider/service/helpers/helper.go index 3f711d55037..f0eb5015f8d 100644 --- a/staging/src/k8s.io/cloud-provider/service/helpers/helper.go +++ b/staging/src/k8s.io/cloud-provider/service/helpers/helper.go @@ -17,10 +17,14 @@ limitations under the License. package helpers import ( + "encoding/json" "fmt" "strings" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/strategicpatch" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" utilnet "k8s.io/utils/net" ) @@ -121,6 +125,40 @@ func LoadBalancerStatusEqual(l, r *v1.LoadBalancerStatus) bool { return ingressSliceEqual(l.Ingress, r.Ingress) } +// PatchService patches the given service's Status or ObjectMeta based on the original and +// updated ones. Change to spec will be ignored. +func PatchService(c corev1.CoreV1Interface, oldSvc, newSvc *v1.Service) (*v1.Service, error) { + // Reset spec to make sure only patch for Status or ObjectMeta. + newSvc.Spec = oldSvc.Spec + + patchBytes, err := getPatchBytes(oldSvc, newSvc) + if err != nil { + return nil, err + } + + return c.Services(oldSvc.Namespace).Patch(oldSvc.Name, types.StrategicMergePatchType, patchBytes, "status") + +} + +func getPatchBytes(oldSvc, newSvc *v1.Service) ([]byte, error) { + oldData, err := json.Marshal(oldSvc) + if err != nil { + return nil, fmt.Errorf("failed to Marshal oldData for svc %s/%s: %v", oldSvc.Namespace, oldSvc.Name, err) + } + + newData, err := json.Marshal(newSvc) + if err != nil { + return nil, fmt.Errorf("failed to Marshal newData for svc %s/%s: %v", newSvc.Namespace, newSvc.Name, err) + } + + patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, v1.Service{}) + if err != nil { + return nil, fmt.Errorf("failed to CreateTwoWayMergePatch for svc %s/%s: %v", oldSvc.Namespace, oldSvc.Name, err) + } + return patchBytes, nil + +} + func ingressSliceEqual(lhs, rhs []v1.LoadBalancerIngress) bool { if len(lhs) != len(rhs) { return false diff --git a/staging/src/k8s.io/cloud-provider/service/helpers/helper_test.go b/staging/src/k8s.io/cloud-provider/service/helpers/helper_test.go index eaf8f7f9749..e96b2fa8759 100644 --- a/staging/src/k8s.io/cloud-provider/service/helpers/helper_test.go +++ b/staging/src/k8s.io/cloud-provider/service/helpers/helper_test.go @@ -17,11 +17,13 @@ limitations under the License. package helpers import ( + "reflect" "strings" "testing" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" utilnet "k8s.io/utils/net" ) @@ -269,3 +271,78 @@ func TestHasLBFinalizer(t *testing.T) { }) } } + +func TestPatchService(t *testing.T) { + svcOrigin := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-patch", + Annotations: map[string]string{}, + }, + Spec: v1.ServiceSpec{ + ClusterIP: "10.0.0.1", + }, + } + fakeCs := fake.NewSimpleClientset(svcOrigin) + + // Issue a separate update and verify patch doesn't fail after this. + svcToUpdate := svcOrigin.DeepCopy() + addAnnotations(svcToUpdate) + if _, err := fakeCs.CoreV1().Services(svcOrigin.Namespace).Update(svcToUpdate); err != nil { + t.Fatalf("Failed to update service: %v", err) + } + + // Attempt to patch based the original service. + svcToPatch := svcOrigin.DeepCopy() + svcToPatch.Finalizers = []string{"foo"} + svcToPatch.Spec.ClusterIP = "10.0.0.2" + svcToPatch.Status = v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + {IP: "8.8.8.8"}, + }, + }, + } + svcPatched, err := PatchService(fakeCs.CoreV1(), svcOrigin, svcToPatch) + if err != nil { + t.Fatalf("Failed to patch service: %v", err) + } + + // Service returned by patch will contain latest content (e.g from + // the separate update). + addAnnotations(svcToPatch) + if !reflect.DeepEqual(svcPatched, svcToPatch) { + t.Errorf("PatchStatus() = %+v, want %+v", svcPatched, svcToPatch) + } + // Explicitly validate if spec is unchanged from origin. + if !reflect.DeepEqual(svcPatched.Spec, svcOrigin.Spec) { + t.Errorf("Got spec = %+v, want %+v", svcPatched.Spec, svcOrigin.Spec) + } +} + +func Test_getPatchBytes(t *testing.T) { + origin := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-patch-bytes", + Finalizers: []string{"foo"}, + }, + } + updated := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-patch-bytes", + Finalizers: []string{"foo", "bar"}, + }, + } + + b, err := getPatchBytes(origin, updated) + if err != nil { + t.Fatal(err) + } + expected := `{"metadata":{"$setElementOrder/finalizers":["foo","bar"],"finalizers":["bar"]}}` + if string(b) != expected { + t.Errorf("getPatchBytes(%+v, %+v) = %s ; want %s", origin, updated, string(b), expected) + } +} + +func addAnnotations(svc *v1.Service) { + svc.Annotations["foo"] = "bar" +}