From f266a308e1251851c51f32b6a0b8774b427f19a9 Mon Sep 17 00:00:00 2001 From: Sean Sullivan Date: Sat, 27 Jul 2019 19:48:15 -0700 Subject: [PATCH] Move pkg/kubectl/polymorphichelpers staging Kubernetes-commit: 9f3384f02f472b0095fe3675139c08e7aaa7e711 --- go.mod | 23 +- go.sum | 8 +- .../attachablepodforobject.go | 54 ++ pkg/polymorphichelpers/canbeexposed.go | 44 ++ pkg/polymorphichelpers/canbeexposed_test.go | 68 +++ pkg/polymorphichelpers/helpers.go | 191 +++++++ pkg/polymorphichelpers/helpers_test.go | 227 ++++++++ pkg/polymorphichelpers/history.go | 390 ++++++++++++++ pkg/polymorphichelpers/history_test.go | 46 ++ pkg/polymorphichelpers/historyviewer.go | 37 ++ pkg/polymorphichelpers/interface.go | 114 ++++ pkg/polymorphichelpers/logsforobject.go | 111 ++++ pkg/polymorphichelpers/logsforobject_test.go | 259 ++++++++++ .../mapbasedselectorforobject.go | 160 ++++++ .../mapbasedselectorforobject_test.go | 489 ++++++++++++++++++ pkg/polymorphichelpers/objectpauser.go | 65 +++ pkg/polymorphichelpers/objectpauser_test.go | 72 +++ pkg/polymorphichelpers/objectrestarter.go | 119 +++++ pkg/polymorphichelpers/objectresumer.go | 64 +++ pkg/polymorphichelpers/objectresumer_test.go | 72 +++ pkg/polymorphichelpers/portsforobject.go | 78 +++ pkg/polymorphichelpers/portsforobject_test.go | 140 +++++ pkg/polymorphichelpers/protocolsforobject.go | 89 ++++ .../protocolsforobject_test.go | 173 +++++++ pkg/polymorphichelpers/rollback.go | 486 +++++++++++++++++ pkg/polymorphichelpers/rollback_test.go | 67 +++ pkg/polymorphichelpers/rollbacker.go | 37 ++ pkg/polymorphichelpers/rollout_status.go | 153 ++++++ pkg/polymorphichelpers/rollout_status_test.go | 469 +++++++++++++++++ pkg/polymorphichelpers/statusviewer.go | 26 + pkg/polymorphichelpers/updatepodspec.go | 91 ++++ pkg/polymorphichelpers/updatepodspec_test.go | 131 +++++ 32 files changed, 4536 insertions(+), 17 deletions(-) create mode 100644 pkg/polymorphichelpers/attachablepodforobject.go create mode 100644 pkg/polymorphichelpers/canbeexposed.go create mode 100644 pkg/polymorphichelpers/canbeexposed_test.go create mode 100644 pkg/polymorphichelpers/helpers.go create mode 100644 pkg/polymorphichelpers/helpers_test.go create mode 100644 pkg/polymorphichelpers/history.go create mode 100644 pkg/polymorphichelpers/history_test.go create mode 100644 pkg/polymorphichelpers/historyviewer.go create mode 100644 pkg/polymorphichelpers/interface.go create mode 100644 pkg/polymorphichelpers/logsforobject.go create mode 100644 pkg/polymorphichelpers/logsforobject_test.go create mode 100644 pkg/polymorphichelpers/mapbasedselectorforobject.go create mode 100644 pkg/polymorphichelpers/mapbasedselectorforobject_test.go create mode 100644 pkg/polymorphichelpers/objectpauser.go create mode 100644 pkg/polymorphichelpers/objectpauser_test.go create mode 100644 pkg/polymorphichelpers/objectrestarter.go create mode 100644 pkg/polymorphichelpers/objectresumer.go create mode 100644 pkg/polymorphichelpers/objectresumer_test.go create mode 100644 pkg/polymorphichelpers/portsforobject.go create mode 100644 pkg/polymorphichelpers/portsforobject_test.go create mode 100644 pkg/polymorphichelpers/protocolsforobject.go create mode 100644 pkg/polymorphichelpers/protocolsforobject_test.go create mode 100644 pkg/polymorphichelpers/rollback.go create mode 100644 pkg/polymorphichelpers/rollback_test.go create mode 100644 pkg/polymorphichelpers/rollbacker.go create mode 100644 pkg/polymorphichelpers/rollout_status.go create mode 100644 pkg/polymorphichelpers/rollout_status_test.go create mode 100644 pkg/polymorphichelpers/statusviewer.go create mode 100644 pkg/polymorphichelpers/updatepodspec.go create mode 100644 pkg/polymorphichelpers/updatepodspec_test.go diff --git a/go.mod b/go.mod index 85ad663ca..b0b22b60b 100644 --- a/go.mod +++ b/go.mod @@ -25,13 +25,13 @@ require ( golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f gopkg.in/yaml.v2 v2.2.2 gotest.tools v2.2.0+incompatible // indirect - k8s.io/api v0.0.0-20190726022912-69e1bce1dad5 - k8s.io/apimachinery v0.0.0-20190727130956-f97a4e5b4abc - k8s.io/cli-runtime v0.0.0-20190726024606-74a61cd71909 - k8s.io/client-go v0.0.0-20190730143201-693ed41095c3 + k8s.io/api v0.0.0 + k8s.io/apimachinery v0.0.0 + k8s.io/cli-runtime v0.0.0 + k8s.io/client-go v0.0.0 k8s.io/klog v0.3.1 k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058 - k8s.io/metrics v0.0.0-20190726024513-9140f5fe6ab8 + k8s.io/metrics v0.0.0 k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a sigs.k8s.io/yaml v1.1.0 ) @@ -43,10 +43,11 @@ replace ( golang.org/x/sys => golang.org/x/sys v0.0.0-20190209173611-3b5209105503 golang.org/x/text => golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db golang.org/x/tools => golang.org/x/tools v0.0.0-20190313210603-aa82965741a9 - k8s.io/api => k8s.io/api v0.0.0-20190726022912-69e1bce1dad5 - k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190727130956-f97a4e5b4abc - k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20190726024606-74a61cd71909 - k8s.io/client-go => k8s.io/client-go v0.0.0-20190730143201-693ed41095c3 - k8s.io/code-generator => k8s.io/code-generator v0.0.0-20190726022633-14ba7d03f06f - k8s.io/metrics => k8s.io/metrics v0.0.0-20190726024513-9140f5fe6ab8 + k8s.io/api => ../api + k8s.io/apimachinery => ../apimachinery + k8s.io/cli-runtime => ../cli-runtime + k8s.io/client-go => ../client-go + k8s.io/code-generator => ../code-generator + k8s.io/kubectl => ../kubectl + k8s.io/metrics => ../metrics ) diff --git a/go.sum b/go.sum index ed5296758..e5e6a80cb 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,7 @@ github.com/go-openapi/swag v0.19.2 h1:jvO6bCMBEilGwMfHhrd61zIID4oIFdwb76V17SM88d github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/gogo/protobuf v0.0.0-20190410021324-65acae22fc9 h1:Kt4R9nQn1c+x/o63vCZuxo3WjBc8EnSfnguI4ELkdoo= github.com/gogo/protobuf v0.0.0-20190410021324-65acae22fc9/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= @@ -77,6 +78,7 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7 h1:6TSoaYExHper8PYsJu23GWVNOyYRCSnIFyxKgLSZ54w= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -194,11 +196,6 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -k8s.io/api v0.0.0-20190726022912-69e1bce1dad5/go.mod h1:V6cpJ9D7WqSy0wqcE096gcbj+W//rshgQgmj1Shdwi8= -k8s.io/apimachinery v0.0.0-20190727130956-f97a4e5b4abc/go.mod h1:eXR4ljjmbwK6Ng0PKsXRySPXnTUy/qBUa6kPDeckhQ0= -k8s.io/cli-runtime v0.0.0-20190726024606-74a61cd71909/go.mod h1:bk/fSEmINmKG2jHCCbqbXmwEJgE6kHVMkOC1U9dclzo= -k8s.io/client-go v0.0.0-20190730143201-693ed41095c3/go.mod h1:WHnpzOmgNDpM2XNcGwWUVwx5ERofFlRgmtg0ljqAwds= -k8s.io/code-generator v0.0.0-20190726022633-14ba7d03f06f/go.mod h1:kr7tMYxZEaP3mrijPwXnhxOvPyqdJw6TZH87KfFboQ0= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= @@ -206,7 +203,6 @@ k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68= k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058 h1:di3XCwddOR9cWBNpfgXaskhh6cgJuwcK54rvtwUaC10= k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= -k8s.io/metrics v0.0.0-20190726024513-9140f5fe6ab8/go.mod h1:qKj9Rmzs1afkjgcziJ2/HzORbC8bXn457iAFxXUDykA= k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a h1:2jUDc9gJja832Ftp+QbDV0tVhQHMISFn01els+2ZAcw= k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= diff --git a/pkg/polymorphichelpers/attachablepodforobject.go b/pkg/polymorphichelpers/attachablepodforobject.go new file mode 100644 index 000000000..7b18111fd --- /dev/null +++ b/pkg/polymorphichelpers/attachablepodforobject.go @@ -0,0 +1,54 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "fmt" + "sort" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/kubectl/pkg/util/podutils" +) + +// attachablePodForObject returns the pod to which to attach given an object. +func attachablePodForObject(restClientGetter genericclioptions.RESTClientGetter, object runtime.Object, timeout time.Duration) (*corev1.Pod, error) { + switch t := object.(type) { + case *corev1.Pod: + return t, nil + } + + clientConfig, err := restClientGetter.ToRESTConfig() + if err != nil { + return nil, err + } + clientset, err := corev1client.NewForConfig(clientConfig) + if err != nil { + return nil, err + } + + namespace, selector, err := SelectorsForObject(object) + if err != nil { + return nil, fmt.Errorf("cannot attach to %T: %v", object, err) + } + sortBy := func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(podutils.ActivePods(pods)) } + pod, _, err := GetFirstPod(clientset, namespace, selector.String(), timeout, sortBy) + return pod, err +} diff --git a/pkg/polymorphichelpers/canbeexposed.go b/pkg/polymorphichelpers/canbeexposed.go new file mode 100644 index 000000000..b232ff853 --- /dev/null +++ b/pkg/polymorphichelpers/canbeexposed.go @@ -0,0 +1,44 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Check whether the kind of resources could be exposed +func canBeExposed(kind schema.GroupKind) error { + switch kind { + case + corev1.SchemeGroupVersion.WithKind("ReplicationController").GroupKind(), + corev1.SchemeGroupVersion.WithKind("Service").GroupKind(), + corev1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind(), + appsv1.SchemeGroupVersion.WithKind("ReplicaSet").GroupKind(), + extensionsv1beta1.SchemeGroupVersion.WithKind("Deployment").GroupKind(), + extensionsv1beta1.SchemeGroupVersion.WithKind("ReplicaSet").GroupKind(): + // nothing to do here + default: + return fmt.Errorf("cannot expose a %s", kind) + } + return nil +} diff --git a/pkg/polymorphichelpers/canbeexposed_test.go b/pkg/polymorphichelpers/canbeexposed_test.go new file mode 100644 index 000000000..b1cd5b2d1 --- /dev/null +++ b/pkg/polymorphichelpers/canbeexposed_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestCanBeExposed(t *testing.T) { + tests := []struct { + kind schema.GroupKind + expectErr bool + }{ + { + kind: corev1.SchemeGroupVersion.WithKind("ReplicationController").GroupKind(), + expectErr: false, + }, + { + kind: corev1.SchemeGroupVersion.WithKind("Service").GroupKind(), + expectErr: false, + }, + { + kind: corev1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + expectErr: false, + }, + { + kind: appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind(), + expectErr: false, + }, + { + kind: extensionsv1beta1.SchemeGroupVersion.WithKind("ReplicaSet").GroupKind(), + expectErr: false, + }, + { + kind: corev1.SchemeGroupVersion.WithKind("Node").GroupKind(), + expectErr: true, + }, + } + + for _, test := range tests { + err := canBeExposed(test.kind) + if test.expectErr && err == nil { + t.Error("unexpected non-error") + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + } +} diff --git a/pkg/polymorphichelpers/helpers.go b/pkg/polymorphichelpers/helpers.go new file mode 100644 index 000000000..27286e71d --- /dev/null +++ b/pkg/polymorphichelpers/helpers.go @@ -0,0 +1,191 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "context" + "fmt" + "sort" + "time" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + coreclient "k8s.io/client-go/kubernetes/typed/core/v1" + watchtools "k8s.io/client-go/tools/watch" +) + +// GetFirstPod returns a pod matching the namespace and label selector +// and the number of all pods that match the label selector. +func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string, timeout time.Duration, sortBy func([]*corev1.Pod) sort.Interface) (*corev1.Pod, int, error) { + options := metav1.ListOptions{LabelSelector: selector} + + podList, err := client.Pods(namespace).List(options) + if err != nil { + return nil, 0, err + } + pods := []*corev1.Pod{} + for i := range podList.Items { + pod := podList.Items[i] + pods = append(pods, &pod) + } + if len(pods) > 0 { + sort.Sort(sortBy(pods)) + return pods[0], len(podList.Items), nil + } + + // Watch until we observe a pod + options.ResourceVersion = podList.ResourceVersion + w, err := client.Pods(namespace).Watch(options) + if err != nil { + return nil, 0, err + } + defer w.Stop() + + condition := func(event watch.Event) (bool, error) { + return event.Type == watch.Added || event.Type == watch.Modified, nil + } + + ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout) + defer cancel() + event, err := watchtools.UntilWithoutRetry(ctx, w, condition) + if err != nil { + return nil, 0, err + } + pod, ok := event.Object.(*corev1.Pod) + if !ok { + return nil, 0, fmt.Errorf("%#v is not a pod event", event) + } + return pod, 1, nil +} + +// SelectorsForObject returns the pod label selector for a given object +func SelectorsForObject(object runtime.Object) (namespace string, selector labels.Selector, err error) { + switch t := object.(type) { + case *extensionsv1beta1.ReplicaSet: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + case *appsv1.ReplicaSet: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + case *appsv1beta2.ReplicaSet: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + + case *corev1.ReplicationController: + namespace = t.Namespace + selector = labels.SelectorFromSet(t.Spec.Selector) + + case *appsv1.StatefulSet: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + case *appsv1beta1.StatefulSet: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + case *appsv1beta2.StatefulSet: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + + case *extensionsv1beta1.DaemonSet: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + case *appsv1.DaemonSet: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + case *appsv1beta2.DaemonSet: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + + case *extensionsv1beta1.Deployment: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + case *appsv1.Deployment: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + case *appsv1beta1.Deployment: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + case *appsv1beta2.Deployment: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + + case *batchv1.Job: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector: %v", err) + } + + case *corev1.Service: + namespace = t.Namespace + if t.Spec.Selector == nil || len(t.Spec.Selector) == 0 { + return "", nil, fmt.Errorf("invalid service '%s': Service is defined without a selector", t.Name) + } + selector = labels.SelectorFromSet(t.Spec.Selector) + + default: + return "", nil, fmt.Errorf("selector for %T not implemented", object) + } + + return namespace, selector, nil +} diff --git a/pkg/polymorphichelpers/helpers_test.go b/pkg/polymorphichelpers/helpers_test.go new file mode 100644 index 000000000..70afa2aca --- /dev/null +++ b/pkg/polymorphichelpers/helpers_test.go @@ -0,0 +1,227 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "fmt" + "sort" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/watch" + fakeexternal "k8s.io/client-go/kubernetes/fake" + testcore "k8s.io/client-go/testing" + "k8s.io/kubectl/pkg/util/podutils" +) + +func TestGetFirstPod(t *testing.T) { + labelSet := map[string]string{"test": "selector"} + tests := []struct { + name string + + podList *corev1.PodList + watching []watch.Event + sortBy func([]*corev1.Pod) sort.Interface + + expected *corev1.Pod + expectedNum int + expectedErr bool + }{ + { + name: "kubectl logs - two ready pods", + podList: newPodList(2, -1, -1, labelSet), + sortBy: func(pods []*corev1.Pod) sort.Interface { return podutils.ByLogging(pods) }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: metav1.NamespaceDefault, + CreationTimestamp: metav1.Date(2016, time.April, 1, 1, 0, 0, 0, time.UTC), + Labels: map[string]string{"test": "selector"}, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Status: corev1.ConditionTrue, + Type: corev1.PodReady, + }, + }, + }, + }, + expectedNum: 2, + }, + { + name: "kubectl logs - one unhealthy, one healthy", + podList: newPodList(2, -1, 1, labelSet), + sortBy: func(pods []*corev1.Pod) sort.Interface { return podutils.ByLogging(pods) }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-2", + Namespace: metav1.NamespaceDefault, + CreationTimestamp: metav1.Date(2016, time.April, 1, 1, 0, 1, 0, time.UTC), + Labels: map[string]string{"test": "selector"}, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Status: corev1.ConditionTrue, + Type: corev1.PodReady, + }, + }, + ContainerStatuses: []corev1.ContainerStatus{{RestartCount: 5}}, + }, + }, + expectedNum: 2, + }, + { + name: "kubectl attach - two ready pods", + podList: newPodList(2, -1, -1, labelSet), + sortBy: func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(podutils.ActivePods(pods)) }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: metav1.NamespaceDefault, + CreationTimestamp: metav1.Date(2016, time.April, 1, 1, 0, 0, 0, time.UTC), + Labels: map[string]string{"test": "selector"}, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Status: corev1.ConditionTrue, + Type: corev1.PodReady, + }, + }, + }, + }, + expectedNum: 2, + }, + { + name: "kubectl attach - wait for ready pod", + podList: newPodList(1, 1, -1, labelSet), + watching: []watch.Event{ + { + Type: watch.Modified, + Object: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: metav1.NamespaceDefault, + CreationTimestamp: metav1.Date(2016, time.April, 1, 1, 0, 0, 0, time.UTC), + Labels: map[string]string{"test": "selector"}, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Status: corev1.ConditionTrue, + Type: corev1.PodReady, + }, + }, + }, + }, + }, + }, + sortBy: func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(podutils.ActivePods(pods)) }, + expected: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: metav1.NamespaceDefault, + CreationTimestamp: metav1.Date(2016, time.April, 1, 1, 0, 0, 0, time.UTC), + Labels: map[string]string{"test": "selector"}, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Status: corev1.ConditionTrue, + Type: corev1.PodReady, + }, + }, + }, + }, + expectedNum: 1, + }, + } + + for i := range tests { + test := tests[i] + fake := fakeexternal.NewSimpleClientset(test.podList) + if len(test.watching) > 0 { + watcher := watch.NewFake() + for _, event := range test.watching { + switch event.Type { + case watch.Added: + go watcher.Add(event.Object) + case watch.Modified: + go watcher.Modify(event.Object) + } + } + fake.PrependWatchReactor("pods", testcore.DefaultWatchReactor(watcher, nil)) + } + selector := labels.Set(labelSet).AsSelector() + + pod, numPods, err := GetFirstPod(fake.CoreV1(), metav1.NamespaceDefault, selector.String(), 1*time.Minute, test.sortBy) + pod.Spec.SecurityContext = nil + if !test.expectedErr && err != nil { + t.Errorf("%s: unexpected error: %v", test.name, err) + continue + } + if test.expectedErr && err == nil { + t.Errorf("%s: expected an error", test.name) + continue + } + if test.expectedNum != numPods { + t.Errorf("%s: expected %d pods, got %d", test.name, test.expectedNum, numPods) + continue + } + if !apiequality.Semantic.DeepEqual(test.expected, pod) { + t.Errorf("%s:\nexpected pod:\n%#v\ngot:\n%#v\n\n", test.name, test.expected, pod) + } + } +} + +func newPodList(count, isUnready, isUnhealthy int, labels map[string]string) *corev1.PodList { + pods := []corev1.Pod{} + for i := 0; i < count; i++ { + newPod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("pod-%d", i+1), + Namespace: metav1.NamespaceDefault, + CreationTimestamp: metav1.Date(2016, time.April, 1, 1, 0, i, 0, time.UTC), + Labels: labels, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Status: corev1.ConditionTrue, + Type: corev1.PodReady, + }, + }, + }, + } + pods = append(pods, newPod) + } + if isUnready > -1 && isUnready < count { + pods[isUnready].Status.Conditions[0].Status = corev1.ConditionFalse + } + if isUnhealthy > -1 && isUnhealthy < count { + pods[isUnhealthy].Status.ContainerStatuses = []corev1.ContainerStatus{{RestartCount: 5}} + } + return &corev1.PodList{ + Items: pods, + } +} diff --git a/pkg/polymorphichelpers/history.go b/pkg/polymorphichelpers/history.go new file mode 100644 index 000000000..cb8986c97 --- /dev/null +++ b/pkg/polymorphichelpers/history.go @@ -0,0 +1,390 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "bytes" + "fmt" + "io" + "text/tabwriter" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/client-go/kubernetes" + clientappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1" + "k8s.io/kubectl/pkg/apps" + describe "k8s.io/kubectl/pkg/describe/versioned" + deploymentutil "k8s.io/kubectl/pkg/util/deployment" + sliceutil "k8s.io/kubectl/pkg/util/slice" +) + +const ( + ChangeCauseAnnotation = "kubernetes.io/change-cause" +) + +// HistoryViewer provides an interface for resources have historical information. +type HistoryViewer interface { + ViewHistory(namespace, name string, revision int64) (string, error) +} + +type HistoryVisitor struct { + clientset kubernetes.Interface + result HistoryViewer +} + +func (v *HistoryVisitor) VisitDeployment(elem apps.GroupKindElement) { + v.result = &DeploymentHistoryViewer{v.clientset} +} + +func (v *HistoryVisitor) VisitStatefulSet(kind apps.GroupKindElement) { + v.result = &StatefulSetHistoryViewer{v.clientset} +} + +func (v *HistoryVisitor) VisitDaemonSet(kind apps.GroupKindElement) { + v.result = &DaemonSetHistoryViewer{v.clientset} +} + +func (v *HistoryVisitor) VisitJob(kind apps.GroupKindElement) {} +func (v *HistoryVisitor) VisitPod(kind apps.GroupKindElement) {} +func (v *HistoryVisitor) VisitReplicaSet(kind apps.GroupKindElement) {} +func (v *HistoryVisitor) VisitReplicationController(kind apps.GroupKindElement) {} +func (v *HistoryVisitor) VisitCronJob(kind apps.GroupKindElement) {} + +// HistoryViewerFor returns an implementation of HistoryViewer interface for the given schema kind +func HistoryViewerFor(kind schema.GroupKind, c kubernetes.Interface) (HistoryViewer, error) { + elem := apps.GroupKindElement(kind) + visitor := &HistoryVisitor{ + clientset: c, + } + + // Determine which HistoryViewer we need here + err := elem.Accept(visitor) + + if err != nil { + return nil, fmt.Errorf("error retrieving history for %q, %v", kind.String(), err) + } + + if visitor.result == nil { + return nil, fmt.Errorf("no history viewer has been implemented for %q", kind.String()) + } + + return visitor.result, nil +} + +type DeploymentHistoryViewer struct { + c kubernetes.Interface +} + +// ViewHistory returns a revision-to-replicaset map as the revision history of a deployment +// TODO: this should be a describer +func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) { + versionedAppsClient := h.c.AppsV1() + deployment, err := versionedAppsClient.Deployments(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return "", fmt.Errorf("failed to retrieve deployment %s: %v", name, err) + } + _, allOldRSs, newRS, err := deploymentutil.GetAllReplicaSets(deployment, versionedAppsClient) + if err != nil { + return "", fmt.Errorf("failed to retrieve replica sets from deployment %s: %v", name, err) + } + allRSs := allOldRSs + if newRS != nil { + allRSs = append(allRSs, newRS) + } + + historyInfo := make(map[int64]*corev1.PodTemplateSpec) + for _, rs := range allRSs { + v, err := deploymentutil.Revision(rs) + if err != nil { + continue + } + historyInfo[v] = &rs.Spec.Template + changeCause := getChangeCause(rs) + if historyInfo[v].Annotations == nil { + historyInfo[v].Annotations = make(map[string]string) + } + if len(changeCause) > 0 { + historyInfo[v].Annotations[ChangeCauseAnnotation] = changeCause + } + } + + if len(historyInfo) == 0 { + return "No rollout history found.", nil + } + + if revision > 0 { + // Print details of a specific revision + template, ok := historyInfo[revision] + if !ok { + return "", fmt.Errorf("unable to find the specified revision") + } + return printTemplate(template) + } + + // Sort the revisionToChangeCause map by revision + revisions := make([]int64, 0, len(historyInfo)) + for r := range historyInfo { + revisions = append(revisions, r) + } + sliceutil.SortInts64(revisions) + + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n") + for _, r := range revisions { + // Find the change-cause of revision r + changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation] + if len(changeCause) == 0 { + changeCause = "" + } + fmt.Fprintf(out, "%d\t%s\n", r, changeCause) + } + return nil + }) +} + +func printTemplate(template *corev1.PodTemplateSpec) (string, error) { + buf := bytes.NewBuffer([]byte{}) + w := describe.NewPrefixWriter(buf) + describe.DescribePodTemplate(template, w) + return buf.String(), nil +} + +type DaemonSetHistoryViewer struct { + c kubernetes.Interface +} + +// ViewHistory returns a revision-to-history map as the revision history of a deployment +// TODO: this should be a describer +func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) { + ds, history, err := daemonSetHistory(h.c.AppsV1(), namespace, name) + if err != nil { + return "", err + } + historyInfo := make(map[int64]*appsv1.ControllerRevision) + for _, history := range history { + // TODO: for now we assume revisions don't overlap, we may need to handle it + historyInfo[history.Revision] = history + } + if len(historyInfo) == 0 { + return "No rollout history found.", nil + } + + // Print details of a specific revision + if revision > 0 { + history, ok := historyInfo[revision] + if !ok { + return "", fmt.Errorf("unable to find the specified revision") + } + dsOfHistory, err := applyDaemonSetHistory(ds, history) + if err != nil { + return "", fmt.Errorf("unable to parse history %s", history.Name) + } + return printTemplate(&dsOfHistory.Spec.Template) + } + + // Print an overview of all Revisions + // Sort the revisionToChangeCause map by revision + revisions := make([]int64, 0, len(historyInfo)) + for r := range historyInfo { + revisions = append(revisions, r) + } + sliceutil.SortInts64(revisions) + + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n") + for _, r := range revisions { + // Find the change-cause of revision r + changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation] + if len(changeCause) == 0 { + changeCause = "" + } + fmt.Fprintf(out, "%d\t%s\n", r, changeCause) + } + return nil + }) +} + +type StatefulSetHistoryViewer struct { + c kubernetes.Interface +} + +// ViewHistory returns a list of the revision history of a statefulset +// TODO: this should be a describer +// TODO: needs to implement detailed revision view +func (h *StatefulSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) { + _, history, err := statefulSetHistory(h.c.AppsV1(), namespace, name) + if err != nil { + return "", err + } + + if len(history) <= 0 { + return "No rollout history found.", nil + } + revisions := make([]int64, len(history)) + for _, revision := range history { + revisions = append(revisions, revision.Revision) + } + sliceutil.SortInts64(revisions) + + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "REVISION\n") + for _, r := range revisions { + fmt.Fprintf(out, "%d\n", r) + } + return nil + }) +} + +// controlledHistories returns all ControllerRevisions in namespace that selected by selector and owned by accessor +// TODO: Rename this to controllerHistory when other controllers have been upgraded +func controlledHistoryV1( + apps clientappsv1.AppsV1Interface, + namespace string, + selector labels.Selector, + accessor metav1.Object) ([]*appsv1.ControllerRevision, error) { + var result []*appsv1.ControllerRevision + historyList, err := apps.ControllerRevisions(namespace).List(metav1.ListOptions{LabelSelector: selector.String()}) + if err != nil { + return nil, err + } + for i := range historyList.Items { + history := historyList.Items[i] + // Only add history that belongs to the API object + if metav1.IsControlledBy(&history, accessor) { + result = append(result, &history) + } + } + return result, nil +} + +// controlledHistories returns all ControllerRevisions in namespace that selected by selector and owned by accessor +func controlledHistory( + apps clientappsv1.AppsV1Interface, + namespace string, + selector labels.Selector, + accessor metav1.Object) ([]*appsv1.ControllerRevision, error) { + var result []*appsv1.ControllerRevision + historyList, err := apps.ControllerRevisions(namespace).List(metav1.ListOptions{LabelSelector: selector.String()}) + if err != nil { + return nil, err + } + for i := range historyList.Items { + history := historyList.Items[i] + // Only add history that belongs to the API object + if metav1.IsControlledBy(&history, accessor) { + result = append(result, &history) + } + } + return result, nil +} + +// daemonSetHistory returns the DaemonSet named name in namespace and all ControllerRevisions in its history. +func daemonSetHistory( + apps clientappsv1.AppsV1Interface, + namespace, name string) (*appsv1.DaemonSet, []*appsv1.ControllerRevision, error) { + ds, err := apps.DaemonSets(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return nil, nil, fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err) + } + selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector) + if err != nil { + return nil, nil, fmt.Errorf("failed to create selector for DaemonSet %s: %v", ds.Name, err) + } + accessor, err := meta.Accessor(ds) + if err != nil { + return nil, nil, fmt.Errorf("failed to create accessor for DaemonSet %s: %v", ds.Name, err) + } + history, err := controlledHistory(apps, ds.Namespace, selector, accessor) + if err != nil { + return nil, nil, fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err) + } + return ds, history, nil +} + +// statefulSetHistory returns the StatefulSet named name in namespace and all ControllerRevisions in its history. +func statefulSetHistory( + apps clientappsv1.AppsV1Interface, + namespace, name string) (*appsv1.StatefulSet, []*appsv1.ControllerRevision, error) { + sts, err := apps.StatefulSets(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return nil, nil, fmt.Errorf("failed to retrieve Statefulset %s: %s", name, err.Error()) + } + selector, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector) + if err != nil { + return nil, nil, fmt.Errorf("failed to create selector for StatefulSet %s: %s", name, err.Error()) + } + accessor, err := meta.Accessor(sts) + if err != nil { + return nil, nil, fmt.Errorf("failed to obtain accessor for StatefulSet %s: %s", name, err.Error()) + } + history, err := controlledHistoryV1(apps, namespace, selector, accessor) + if err != nil { + return nil, nil, fmt.Errorf("unable to find history controlled by StatefulSet %s: %v", name, err) + } + return sts, history, nil +} + +// applyDaemonSetHistory returns a specific revision of DaemonSet by applying the given history to a copy of the given DaemonSet +func applyDaemonSetHistory(ds *appsv1.DaemonSet, history *appsv1.ControllerRevision) (*appsv1.DaemonSet, error) { + clone := ds.DeepCopy() + cloneBytes, err := json.Marshal(clone) + if err != nil { + return nil, err + } + patched, err := strategicpatch.StrategicMergePatch(cloneBytes, history.Data.Raw, clone) + if err != nil { + return nil, err + } + err = json.Unmarshal(patched, clone) + if err != nil { + return nil, err + } + return clone, nil +} + +// TODO: copied here until this becomes a describer +func tabbedString(f func(io.Writer) error) (string, error) { + out := new(tabwriter.Writer) + buf := &bytes.Buffer{} + out.Init(buf, 0, 8, 2, ' ', 0) + + err := f(out) + if err != nil { + return "", err + } + + out.Flush() + str := string(buf.String()) + return str, nil +} + +// getChangeCause returns the change-cause annotation of the input object +func getChangeCause(obj runtime.Object) string { + accessor, err := meta.Accessor(obj) + if err != nil { + return "" + } + return accessor.GetAnnotations()[ChangeCauseAnnotation] +} diff --git a/pkg/polymorphichelpers/history_test.go b/pkg/polymorphichelpers/history_test.go new file mode 100644 index 000000000..9bfcaece8 --- /dev/null +++ b/pkg/polymorphichelpers/history_test.go @@ -0,0 +1,46 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes/fake" +) + +var historytests = map[schema.GroupKind]reflect.Type{ + {Group: "apps", Kind: "DaemonSet"}: reflect.TypeOf(&DaemonSetHistoryViewer{}), + {Group: "apps", Kind: "StatefulSet"}: reflect.TypeOf(&StatefulSetHistoryViewer{}), + {Group: "apps", Kind: "Deployment"}: reflect.TypeOf(&DeploymentHistoryViewer{}), +} + +func TestHistoryViewerFor(t *testing.T) { + fakeClientset := &fake.Clientset{} + + for kind, expectedType := range historytests { + result, err := HistoryViewerFor(kind, fakeClientset) + if err != nil { + t.Fatalf("error getting HistoryViewer for a %v: %v", kind.String(), err) + } + + if reflect.TypeOf(result) != expectedType { + t.Fatalf("unexpected output type (%v was expected but got %v)", expectedType, reflect.TypeOf(result)) + } + } +} diff --git a/pkg/polymorphichelpers/historyviewer.go b/pkg/polymorphichelpers/historyviewer.go new file mode 100644 index 000000000..6ad9d217c --- /dev/null +++ b/pkg/polymorphichelpers/historyviewer.go @@ -0,0 +1,37 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" +) + +// historyViewer Returns a HistoryViewer for viewing change history +func historyViewer(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (HistoryViewer, error) { + clientConfig, err := restClientGetter.ToRESTConfig() + if err != nil { + return nil, err + } + + external, err := kubernetes.NewForConfig(clientConfig) + if err != nil { + return nil, err + } + return HistoryViewerFor(mapping.GroupVersionKind.GroupKind(), external) +} diff --git a/pkg/polymorphichelpers/interface.go b/pkg/polymorphichelpers/interface.go new file mode 100644 index 000000000..425d68f5b --- /dev/null +++ b/pkg/polymorphichelpers/interface.go @@ -0,0 +1,114 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "time" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/rest" +) + +// LogsForObjectFunc is a function type that can tell you how to get logs for a runtime.object +type LogsForObjectFunc func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) ([]rest.ResponseWrapper, error) + +// LogsForObjectFn gives a way to easily override the function for unit testing if needed. +var LogsForObjectFn LogsForObjectFunc = logsForObject + +// AttachablePodForObjectFunc is a function type that can tell you how to get the pod for which to attach a given object +type AttachablePodForObjectFunc func(restClientGetter genericclioptions.RESTClientGetter, object runtime.Object, timeout time.Duration) (*v1.Pod, error) + +// AttachablePodForObjectFn gives a way to easily override the function for unit testing if needed. +var AttachablePodForObjectFn AttachablePodForObjectFunc = attachablePodForObject + +// HistoryViewerFunc is a function type that can tell you how to view change history +type HistoryViewerFunc func(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (HistoryViewer, error) + +// HistoryViewerFn gives a way to easily override the function for unit testing if needed +var HistoryViewerFn HistoryViewerFunc = historyViewer + +// StatusViewerFunc is a function type that can tell you how to print rollout status +type StatusViewerFunc func(mapping *meta.RESTMapping) (StatusViewer, error) + +// StatusViewerFn gives a way to easily override the function for unit testing if needed +var StatusViewerFn StatusViewerFunc = statusViewer + +// UpdatePodSpecForObjectFunc will call the provided function on the pod spec this object supports, +// return false if no pod spec is supported, or return an error. +type UpdatePodSpecForObjectFunc func(obj runtime.Object, fn func(*v1.PodSpec) error) (bool, error) + +// UpdatePodSpecForObjectFn gives a way to easily override the function for unit testing if needed +var UpdatePodSpecForObjectFn UpdatePodSpecForObjectFunc = updatePodSpecForObject + +// MapBasedSelectorForObjectFunc will call the provided function on mapping the baesd selector for object, +// return "" if object is not supported, or return an error. +type MapBasedSelectorForObjectFunc func(object runtime.Object) (string, error) + +// MapBasedSelectorForObjectFn gives a way to easily override the function for unit testing if needed +var MapBasedSelectorForObjectFn MapBasedSelectorForObjectFunc = mapBasedSelectorForObject + +// ProtocolsForObjectFunc will call the provided function on the protocols for the object, +// return nil-map if no protocols for the object, or return an error. +type ProtocolsForObjectFunc func(object runtime.Object) (map[string]string, error) + +// ProtocolsForObjectFn gives a way to easily override the function for unit testing if needed +var ProtocolsForObjectFn ProtocolsForObjectFunc = protocolsForObject + +// PortsForObjectFunc returns the ports associated with the provided object +type PortsForObjectFunc func(object runtime.Object) ([]string, error) + +// PortsForObjectFn gives a way to easily override the function for unit testing if needed +var PortsForObjectFn PortsForObjectFunc = portsForObject + +// CanBeExposedFunc is a function type that can tell you whether a given GroupKind is capable of being exposed +type CanBeExposedFunc func(kind schema.GroupKind) error + +// CanBeExposedFn gives a way to easily override the function for unit testing if needed +var CanBeExposedFn CanBeExposedFunc = canBeExposed + +// ObjectPauserFunc is a function type that marks the object in a given info as paused. +type ObjectPauserFunc func(runtime.Object) ([]byte, error) + +// ObjectPauserFn gives a way to easily override the function for unit testing if needed. +// Returns the patched object in bytes and any error that occurred during the encoding or +// in case the object is already paused. +var ObjectPauserFn ObjectPauserFunc = defaultObjectPauser + +// ObjectResumerFunc is a function type that marks the object in a given info as resumed. +type ObjectResumerFunc func(runtime.Object) ([]byte, error) + +// ObjectResumerFn gives a way to easily override the function for unit testing if needed. +// Returns the patched object in bytes and any error that occurred during the encoding or +// in case the object is already resumed. +var ObjectResumerFn ObjectResumerFunc = defaultObjectResumer + +// RollbackerFunc gives a way to change the rollback version of the specified RESTMapping type +type RollbackerFunc func(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (Rollbacker, error) + +// RollbackerFn gives a way to easily override the function for unit testing if needed +var RollbackerFn RollbackerFunc = rollbacker + +// ObjectRestarterFunc is a function type that updates an annotation in a deployment to restart it.. +type ObjectRestarterFunc func(runtime.Object) ([]byte, error) + +// ObjectRestarterFn gives a way to easily override the function for unit testing if needed. +// Returns the patched object in bytes and any error that occurred during the encoding. +var ObjectRestarterFn ObjectRestarterFunc = defaultObjectRestarter diff --git a/pkg/polymorphichelpers/logsforobject.go b/pkg/polymorphichelpers/logsforobject.go new file mode 100644 index 000000000..687301915 --- /dev/null +++ b/pkg/polymorphichelpers/logsforobject.go @@ -0,0 +1,111 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "errors" + "fmt" + "os" + "sort" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" + "k8s.io/kubectl/pkg/util/podutils" +) + +func logsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) ([]rest.ResponseWrapper, error) { + clientConfig, err := restClientGetter.ToRESTConfig() + if err != nil { + return nil, err + } + + clientset, err := corev1client.NewForConfig(clientConfig) + if err != nil { + return nil, err + } + return logsForObjectWithClient(clientset, object, options, timeout, allContainers) +} + +// TODO: remove internal clientset once all callers use external versions +// this is split for easy test-ability +func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, options runtime.Object, timeout time.Duration, allContainers bool) ([]rest.ResponseWrapper, error) { + opts, ok := options.(*corev1.PodLogOptions) + if !ok { + return nil, errors.New("provided options object is not a PodLogOptions") + } + + switch t := object.(type) { + case *corev1.PodList: + ret := []rest.ResponseWrapper{} + for i := range t.Items { + currRet, err := logsForObjectWithClient(clientset, &t.Items[i], options, timeout, allContainers) + if err != nil { + return nil, err + } + ret = append(ret, currRet...) + } + return ret, nil + + case *corev1.Pod: + // if allContainers is true, then we're going to locate all containers and then iterate through them. At that point, "allContainers" is false + if !allContainers { + return []rest.ResponseWrapper{clientset.Pods(t.Namespace).GetLogs(t.Name, opts)}, nil + } + + ret := []rest.ResponseWrapper{} + for _, c := range t.Spec.InitContainers { + currOpts := opts.DeepCopy() + currOpts.Container = c.Name + currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false) + if err != nil { + return nil, err + } + ret = append(ret, currRet...) + } + for _, c := range t.Spec.Containers { + currOpts := opts.DeepCopy() + currOpts.Container = c.Name + currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false) + if err != nil { + return nil, err + } + ret = append(ret, currRet...) + } + + return ret, nil + } + + namespace, selector, err := SelectorsForObject(object) + if err != nil { + return nil, fmt.Errorf("cannot get the logs from %T: %v", object, err) + } + + sortBy := func(pods []*corev1.Pod) sort.Interface { return podutils.ByLogging(pods) } + pod, numPods, err := GetFirstPod(clientset, namespace, selector.String(), timeout, sortBy) + if err != nil { + return nil, err + } + if numPods > 1 { + fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name) + } + + return logsForObjectWithClient(clientset, pod, options, timeout, allContainers) +} diff --git a/pkg/polymorphichelpers/logsforobject_test.go b/pkg/polymorphichelpers/logsforobject_test.go new file mode 100644 index 000000000..7a75d8bb5 --- /dev/null +++ b/pkg/polymorphichelpers/logsforobject_test.go @@ -0,0 +1,259 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "reflect" + "testing" + "time" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/diff" + fakeexternal "k8s.io/client-go/kubernetes/fake" + testclient "k8s.io/client-go/testing" +) + +var ( + podsResource = schema.GroupVersionResource{Version: "v1", Resource: "pods"} + podsKind = schema.GroupVersionKind{Version: "v1", Kind: "Pod"} +) + +func TestLogsForObject(t *testing.T) { + tests := []struct { + name string + obj runtime.Object + opts *corev1.PodLogOptions + allContainers bool + pods []runtime.Object + actions []testclient.Action + }{ + { + name: "pod logs", + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + getLogsAction("test", nil), + }, + }, + { + name: "pod logs: all containers", + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + {Name: "initc1"}, + {Name: "initc2"}, + }, + Containers: []corev1.Container{ + {Name: "c1"}, + {Name: "c2"}, + }, + }, + }, + opts: &corev1.PodLogOptions{}, + allContainers: true, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + getLogsAction("test", &corev1.PodLogOptions{Container: "initc1"}), + getLogsAction("test", &corev1.PodLogOptions{Container: "initc2"}), + getLogsAction("test", &corev1.PodLogOptions{Container: "c1"}), + getLogsAction("test", &corev1.PodLogOptions{Container: "c2"}), + }, + }, + { + name: "pods list logs", + obj: &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + {Name: "initc1"}, + {Name: "initc2"}, + }, + Containers: []corev1.Container{ + {Name: "c1"}, + {Name: "c2"}, + }, + }, + }, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + getLogsAction("test", nil), + }, + }, + { + name: "pods list logs: all containers", + obj: &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + {Name: "initc1"}, + {Name: "initc2"}, + }, + Containers: []corev1.Container{ + {Name: "c1"}, + {Name: "c2"}, + }, + }, + }, + }, + }, + opts: &corev1.PodLogOptions{}, + allContainers: true, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + getLogsAction("test", &corev1.PodLogOptions{Container: "initc1"}), + getLogsAction("test", &corev1.PodLogOptions{Container: "initc2"}), + getLogsAction("test", &corev1.PodLogOptions{Container: "c1"}), + getLogsAction("test", &corev1.PodLogOptions{Container: "c2"}), + }, + }, + { + name: "replication controller logs", + obj: &corev1.ReplicationController{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: corev1.ReplicationControllerSpec{ + Selector: map[string]string{"foo": "bar"}, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + testclient.NewListAction(podsResource, podsKind, "test", metav1.ListOptions{LabelSelector: "foo=bar"}), + getLogsAction("test", nil), + }, + }, + { + name: "replica set logs", + obj: &extensionsv1beta1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: extensionsv1beta1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + testclient.NewListAction(podsResource, podsKind, "test", metav1.ListOptions{LabelSelector: "foo=bar"}), + getLogsAction("test", nil), + }, + }, + { + name: "deployment logs", + obj: &extensionsv1beta1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: extensionsv1beta1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + testclient.NewListAction(podsResource, podsKind, "test", metav1.ListOptions{LabelSelector: "foo=bar"}), + getLogsAction("test", nil), + }, + }, + { + name: "job logs", + obj: &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: batchv1.JobSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + testclient.NewListAction(podsResource, podsKind, "test", metav1.ListOptions{LabelSelector: "foo=bar"}), + getLogsAction("test", nil), + }, + }, + { + name: "stateful set logs", + obj: &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: appsv1.StatefulSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + testclient.NewListAction(podsResource, podsKind, "test", metav1.ListOptions{LabelSelector: "foo=bar"}), + getLogsAction("test", nil), + }, + }, + } + + for _, test := range tests { + fakeClientset := fakeexternal.NewSimpleClientset(test.pods...) + _, err := logsForObjectWithClient(fakeClientset.CoreV1(), test.obj, test.opts, 20*time.Second, test.allContainers) + if err != nil { + t.Errorf("%s: unexpected error: %v", test.name, err) + continue + } + + for i := range test.actions { + if len(fakeClientset.Actions()) < i { + t.Errorf("%s: action %d does not exists in actual actions: %#v", + test.name, i, fakeClientset.Actions()) + continue + } + got := fakeClientset.Actions()[i] + want := test.actions[i] + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: unexpected action: %s", test.name, diff.ObjectDiff(got, want)) + } + } + } +} + +func testPod() runtime.Object { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "test", + Labels: map[string]string{"foo": "bar"}, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyAlways, + DNSPolicy: corev1.DNSClusterFirst, + Containers: []corev1.Container{ + {Name: "c1"}, + {Name: "c2"}, + }, + }, + } +} + +func getLogsAction(namespace string, opts *corev1.PodLogOptions) testclient.Action { + action := testclient.GenericActionImpl{} + action.Verb = "get" + action.Namespace = namespace + action.Resource = podsResource + action.Subresource = "log" + action.Value = opts + return action +} diff --git a/pkg/polymorphichelpers/mapbasedselectorforobject.go b/pkg/polymorphichelpers/mapbasedselectorforobject.go new file mode 100644 index 000000000..729ae6d4f --- /dev/null +++ b/pkg/polymorphichelpers/mapbasedselectorforobject.go @@ -0,0 +1,160 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "fmt" + "strings" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +// mapBasedSelectorForObject returns the map-based selector associated with the provided object. If a +// new set-based selector is provided, an error is returned if the selector cannot be converted to a +// map-based selector +func mapBasedSelectorForObject(object runtime.Object) (string, error) { + // TODO: replace with a swagger schema based approach (identify pod selector via schema introspection) + switch t := object.(type) { + case *corev1.ReplicationController: + return MakeLabels(t.Spec.Selector), nil + + case *corev1.Pod: + if len(t.Labels) == 0 { + return "", fmt.Errorf("the pod has no labels and cannot be exposed") + } + return MakeLabels(t.Labels), nil + + case *corev1.Service: + if t.Spec.Selector == nil { + return "", fmt.Errorf("the service has no pod selector set") + } + return MakeLabels(t.Spec.Selector), nil + + case *extensionsv1beta1.Deployment: + // "extensions" deployments use pod template labels if selector is not set. + var labels map[string]string + if t.Spec.Selector != nil { + // TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals + // operator, DoubleEquals operator and In operator with only one element in the set. + if len(t.Spec.Selector.MatchExpressions) > 0 { + return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions) + } + labels = t.Spec.Selector.MatchLabels + } else { + labels = t.Spec.Template.Labels + } + if len(labels) == 0 { + return "", fmt.Errorf("the deployment has no labels or selectors and cannot be exposed") + } + return MakeLabels(labels), nil + + case *appsv1.Deployment: + // "apps" deployments must have the selector set. + if t.Spec.Selector == nil || len(t.Spec.Selector.MatchLabels) == 0 { + return "", fmt.Errorf("invalid deployment: no selectors, therefore cannot be exposed") + } + // TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals + // operator, DoubleEquals operator and In operator with only one element in the set. + if len(t.Spec.Selector.MatchExpressions) > 0 { + return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions) + } + return MakeLabels(t.Spec.Selector.MatchLabels), nil + + case *appsv1beta2.Deployment: + // "apps" deployments must have the selector set. + if t.Spec.Selector == nil || len(t.Spec.Selector.MatchLabels) == 0 { + return "", fmt.Errorf("invalid deployment: no selectors, therefore cannot be exposed") + } + // TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals + // operator, DoubleEquals operator and In operator with only one element in the set. + if len(t.Spec.Selector.MatchExpressions) > 0 { + return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions) + } + return MakeLabels(t.Spec.Selector.MatchLabels), nil + + case *appsv1beta1.Deployment: + // "apps" deployments must have the selector set. + if t.Spec.Selector == nil || len(t.Spec.Selector.MatchLabels) == 0 { + return "", fmt.Errorf("invalid deployment: no selectors, therefore cannot be exposed") + } + // TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals + // operator, DoubleEquals operator and In operator with only one element in the set. + if len(t.Spec.Selector.MatchExpressions) > 0 { + return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions) + } + return MakeLabels(t.Spec.Selector.MatchLabels), nil + + case *extensionsv1beta1.ReplicaSet: + // "extensions" replicasets use pod template labels if selector is not set. + var labels map[string]string + if t.Spec.Selector != nil { + // TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals + // operator, DoubleEquals operator and In operator with only one element in the set. + if len(t.Spec.Selector.MatchExpressions) > 0 { + return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions) + } + labels = t.Spec.Selector.MatchLabels + } else { + labels = t.Spec.Template.Labels + } + if len(labels) == 0 { + return "", fmt.Errorf("the replica set has no labels or selectors and cannot be exposed") + } + return MakeLabels(labels), nil + + case *appsv1.ReplicaSet: + // "apps" replicasets must have the selector set. + if t.Spec.Selector == nil || len(t.Spec.Selector.MatchLabels) == 0 { + return "", fmt.Errorf("invalid replicaset: no selectors, therefore cannot be exposed") + } + // TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals + // operator, DoubleEquals operator and In operator with only one element in the set. + if len(t.Spec.Selector.MatchExpressions) > 0 { + return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions) + } + return MakeLabels(t.Spec.Selector.MatchLabels), nil + + case *appsv1beta2.ReplicaSet: + // "apps" replicasets must have the selector set. + if t.Spec.Selector == nil || len(t.Spec.Selector.MatchLabels) == 0 { + return "", fmt.Errorf("invalid replicaset: no selectors, therefore cannot be exposed") + } + // TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals + // operator, DoubleEquals operator and In operator with only one element in the set. + if len(t.Spec.Selector.MatchExpressions) > 0 { + return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions) + } + return MakeLabels(t.Spec.Selector.MatchLabels), nil + + default: + return "", fmt.Errorf("cannot extract pod selector from %T", object) + } + +} + +func MakeLabels(labels map[string]string) string { + out := []string{} + for key, value := range labels { + out = append(out, fmt.Sprintf("%s=%s", key, value)) + } + return strings.Join(out, ",") +} diff --git a/pkg/polymorphichelpers/mapbasedselectorforobject_test.go b/pkg/polymorphichelpers/mapbasedselectorforobject_test.go new file mode 100644 index 000000000..96044d643 --- /dev/null +++ b/pkg/polymorphichelpers/mapbasedselectorforobject_test.go @@ -0,0 +1,489 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "testing" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestMapBasedSelectorForObject(t *testing.T) { + tests := []struct { + object runtime.Object + expectSelector string + expectErr bool + }{ + { + object: &corev1.ReplicationController{ + Spec: corev1.ReplicationControllerSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + }, + }, + expectSelector: "foo=bar", + }, + { + object: &corev1.Pod{}, + expectErr: true, + }, + { + object: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + expectSelector: "foo=bar", + }, + { + object: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + }, + }, + expectSelector: "foo=bar", + }, + { + object: &corev1.Service{}, + expectErr: true, + }, + // extensions/v1beta1 Deployment with labels and selectors + { + object: &extensionsv1beta1.Deployment{ + Spec: extensionsv1beta1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + // extensions/v1beta1 Deployment with only labels (no selectors) -- use labels + { + object: &extensionsv1beta1.Deployment{ + Spec: extensionsv1beta1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + // extensions/v1beta1 Deployment with bad selector + { + object: &extensionsv1beta1.Deployment{ + Spec: extensionsv1beta1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "foo", + }, + }, + }, + }, + }, + expectErr: true, + }, + // apps/v1 Deployment with labels and selectors + { + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + // apps/v1 Deployment with only labels (no selectors) -- error + { + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectErr: true, + }, + // apps/v1 Deployment with no labels or selectors -- error + { + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{}, + }, + expectErr: true, + }, + // apps/v1 Deployment with empty labels -- error + { + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{}, // Empty labels map + }, + }, + }, + }, + expectErr: true, + }, + // apps/v1beta2 Deployment with labels and selectors + { + object: &appsv1beta2.Deployment{ + Spec: appsv1beta2.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + // apps/v1beta2 Deployment with only labels (no selectors) -- error + { + object: &appsv1beta2.Deployment{ + Spec: appsv1beta2.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectErr: true, + }, + // apps/v1beta2 Deployment with no labels or selectors -- error + { + object: &appsv1beta2.Deployment{ + Spec: appsv1beta2.DeploymentSpec{}, + }, + expectErr: true, + }, + // apps/v1beta1 Deployment with labels and selectors + { + object: &appsv1beta1.Deployment{ + Spec: appsv1beta1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + // apps/v1beta1 Deployment with only labels (no selectors) -- error + { + object: &appsv1beta1.Deployment{ + Spec: appsv1beta1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectErr: true, + }, + // apps/v1beta1 Deployment with no labels or selectors -- error + { + object: &appsv1beta1.Deployment{ + Spec: appsv1beta1.DeploymentSpec{}, + }, + expectErr: true, + }, + // extensions/v1beta1 ReplicaSet with labels and selectors + { + object: &extensionsv1beta1.ReplicaSet{ + Spec: extensionsv1beta1.ReplicaSetSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + // extensions/v1beta1 ReplicaSet with only labels -- no selectors; use labels + { + object: &extensionsv1beta1.ReplicaSet{ + Spec: extensionsv1beta1.ReplicaSetSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + // extensions/v1beta1 ReplicaSet with bad label selector -- error + { + object: &extensionsv1beta1.ReplicaSet{ + Spec: extensionsv1beta1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "foo", + }, + }, + }, + }, + }, + expectErr: true, + }, + // apps/v1 ReplicaSet with labels and selectors + { + object: &appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + // apps/v1 ReplicaSet with only labels (no selectors) -- error + { + object: &appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectErr: true, + }, + // apps/v1beta2 ReplicaSet with labels and selectors + { + object: &appsv1beta2.ReplicaSet{ + Spec: appsv1beta2.ReplicaSetSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + // apps/v1beta2 ReplicaSet with only labels (no selectors) -- error + { + object: &appsv1beta2.ReplicaSet{ + Spec: appsv1beta2.ReplicaSetSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectErr: true, + }, + // Node can not be exposed -- error + { + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + { + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "foo", + }, + }, + }, + }, + }, + expectErr: true, + }, + { + object: &appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + expectSelector: "foo=bar", + }, + { + object: &appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "foo", + }, + }, + }, + }, + }, + expectErr: true, + }, + + { + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Selector: nil, + }, + }, + expectErr: true, + }, + { + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Selector: nil, + }, + }, + expectErr: true, + }, + { + object: &appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Selector: nil, + }, + }, + expectErr: true, + }, + { + object: &appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Selector: nil, + }, + }, + expectErr: true, + }, + + { + object: &corev1.Node{}, + expectErr: true, + }, + } + + for _, test := range tests { + actual, err := mapBasedSelectorForObject(test.object) + if test.expectErr && err == nil { + t.Error("unexpected non-error") + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + if actual != test.expectSelector { + t.Errorf("expected selector %q, but got %q", test.expectSelector, actual) + } + } +} diff --git a/pkg/polymorphichelpers/objectpauser.go b/pkg/polymorphichelpers/objectpauser.go new file mode 100644 index 000000000..f50daf255 --- /dev/null +++ b/pkg/polymorphichelpers/objectpauser.go @@ -0,0 +1,65 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "errors" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubectl/pkg/scheme" +) + +// Currently only supports Deployments. +func defaultObjectPauser(obj runtime.Object) ([]byte, error) { + switch obj := obj.(type) { + case *extensionsv1beta1.Deployment: + if obj.Spec.Paused { + return nil, errors.New("is already paused") + } + obj.Spec.Paused = true + return runtime.Encode(scheme.Codecs.LegacyCodec(extensionsv1beta1.SchemeGroupVersion), obj) + + case *appsv1.Deployment: + if obj.Spec.Paused { + return nil, errors.New("is already paused") + } + obj.Spec.Paused = true + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj) + + case *appsv1beta2.Deployment: + if obj.Spec.Paused { + return nil, errors.New("is already paused") + } + obj.Spec.Paused = true + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj) + + case *appsv1beta1.Deployment: + if obj.Spec.Paused { + return nil, errors.New("is already paused") + } + obj.Spec.Paused = true + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta1.SchemeGroupVersion), obj) + + default: + return nil, fmt.Errorf("pausing is not supported") + } +} diff --git a/pkg/polymorphichelpers/objectpauser_test.go b/pkg/polymorphichelpers/objectpauser_test.go new file mode 100644 index 000000000..c141224f1 --- /dev/null +++ b/pkg/polymorphichelpers/objectpauser_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "bytes" + "testing" + + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestDefaultObjectPauser(t *testing.T) { + tests := []struct { + object runtime.Object + expect []byte + expectErr bool + }{ + { + object: &extensionsv1beta1.Deployment{ + Spec: extensionsv1beta1.DeploymentSpec{ + Paused: false, + }, + }, + expect: []byte(`paused":true`), + expectErr: false, + }, + { + object: &extensionsv1beta1.Deployment{ + Spec: extensionsv1beta1.DeploymentSpec{ + Paused: true, + }, + }, + expectErr: true, + }, + { + object: &extensionsv1beta1.ReplicaSet{}, + expectErr: true, + }, + } + + for _, test := range tests { + actual, err := defaultObjectPauser(test.object) + if test.expectErr { + if err == nil { + t.Error("unexpected non-error") + } + continue + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + continue + } + if !bytes.Contains(actual, test.expect) { + t.Errorf("expected %s, but got %s", test.expect, actual) + } + } +} diff --git a/pkg/polymorphichelpers/objectrestarter.go b/pkg/polymorphichelpers/objectrestarter.go new file mode 100644 index 000000000..cbcf7c882 --- /dev/null +++ b/pkg/polymorphichelpers/objectrestarter.go @@ -0,0 +1,119 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "errors" + "fmt" + "time" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubectl/pkg/scheme" +) + +func defaultObjectRestarter(obj runtime.Object) ([]byte, error) { + switch obj := obj.(type) { + case *extensionsv1beta1.Deployment: + if obj.Spec.Paused { + return nil, errors.New("can't restart paused deployment (run rollout resume first)") + } + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(extensionsv1beta1.SchemeGroupVersion), obj) + + case *appsv1.Deployment: + if obj.Spec.Paused { + return nil, errors.New("can't restart paused deployment (run rollout resume first)") + } + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj) + + case *appsv1beta2.Deployment: + if obj.Spec.Paused { + return nil, errors.New("can't restart paused deployment (run rollout resume first)") + } + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj) + + case *appsv1beta1.Deployment: + if obj.Spec.Paused { + return nil, errors.New("can't restart paused deployment (run rollout resume first)") + } + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta1.SchemeGroupVersion), obj) + + case *extensionsv1beta1.DaemonSet: + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(extensionsv1beta1.SchemeGroupVersion), obj) + + case *appsv1.DaemonSet: + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj) + + case *appsv1beta2.DaemonSet: + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj) + + case *appsv1.StatefulSet: + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj) + + case *appsv1beta1.StatefulSet: + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta1.SchemeGroupVersion), obj) + + case *appsv1beta2.StatefulSet: + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj) + + default: + return nil, fmt.Errorf("restarting is not supported") + } +} diff --git a/pkg/polymorphichelpers/objectresumer.go b/pkg/polymorphichelpers/objectresumer.go new file mode 100644 index 000000000..783da6125 --- /dev/null +++ b/pkg/polymorphichelpers/objectresumer.go @@ -0,0 +1,64 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "errors" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubectl/pkg/scheme" +) + +func defaultObjectResumer(obj runtime.Object) ([]byte, error) { + switch obj := obj.(type) { + case *extensionsv1beta1.Deployment: + if !obj.Spec.Paused { + return nil, errors.New("is not paused") + } + obj.Spec.Paused = false + return runtime.Encode(scheme.Codecs.LegacyCodec(extensionsv1beta1.SchemeGroupVersion), obj) + + case *appsv1.Deployment: + if !obj.Spec.Paused { + return nil, errors.New("is not paused") + } + obj.Spec.Paused = false + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj) + + case *appsv1beta2.Deployment: + if !obj.Spec.Paused { + return nil, errors.New("is not paused") + } + obj.Spec.Paused = false + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj) + + case *appsv1beta1.Deployment: + if !obj.Spec.Paused { + return nil, errors.New("is not paused") + } + obj.Spec.Paused = false + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta1.SchemeGroupVersion), obj) + + default: + return nil, fmt.Errorf("resuming is not supported") + } +} diff --git a/pkg/polymorphichelpers/objectresumer_test.go b/pkg/polymorphichelpers/objectresumer_test.go new file mode 100644 index 000000000..408e961a4 --- /dev/null +++ b/pkg/polymorphichelpers/objectresumer_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "bytes" + "testing" + + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestDefaultObjectResumer(t *testing.T) { + tests := []struct { + object runtime.Object + notHave []byte + expectErr bool + }{ + { + object: &extensionsv1beta1.Deployment{ + Spec: extensionsv1beta1.DeploymentSpec{ + Paused: true, + }, + }, + notHave: []byte(`paused":true`), + expectErr: false, + }, + { + object: &extensionsv1beta1.Deployment{ + Spec: extensionsv1beta1.DeploymentSpec{ + Paused: false, + }, + }, + expectErr: true, + }, + { + object: &extensionsv1beta1.ReplicaSet{}, + expectErr: true, + }, + } + + for _, test := range tests { + actual, err := defaultObjectResumer(test.object) + if test.expectErr { + if err == nil { + t.Error("unexpected non-error") + } + continue + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + continue + } + if bytes.Contains(actual, test.notHave) { + t.Errorf("expected to not have %s, but got %s", test.notHave, actual) + } + } +} diff --git a/pkg/polymorphichelpers/portsforobject.go b/pkg/polymorphichelpers/portsforobject.go new file mode 100644 index 000000000..6cc9a2a4e --- /dev/null +++ b/pkg/polymorphichelpers/portsforobject.go @@ -0,0 +1,78 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "fmt" + "strconv" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func portsForObject(object runtime.Object) ([]string, error) { + switch t := object.(type) { + case *corev1.ReplicationController: + return getPorts(t.Spec.Template.Spec), nil + + case *corev1.Pod: + return getPorts(t.Spec), nil + + case *corev1.Service: + return getServicePorts(t.Spec), nil + + case *extensionsv1beta1.Deployment: + return getPorts(t.Spec.Template.Spec), nil + case *appsv1.Deployment: + return getPorts(t.Spec.Template.Spec), nil + case *appsv1beta2.Deployment: + return getPorts(t.Spec.Template.Spec), nil + case *appsv1beta1.Deployment: + return getPorts(t.Spec.Template.Spec), nil + + case *extensionsv1beta1.ReplicaSet: + return getPorts(t.Spec.Template.Spec), nil + case *appsv1.ReplicaSet: + return getPorts(t.Spec.Template.Spec), nil + case *appsv1beta2.ReplicaSet: + return getPorts(t.Spec.Template.Spec), nil + default: + return nil, fmt.Errorf("cannot extract ports from %T", object) + } +} + +func getPorts(spec corev1.PodSpec) []string { + result := []string{} + for _, container := range spec.Containers { + for _, port := range container.Ports { + result = append(result, strconv.Itoa(int(port.ContainerPort))) + } + } + return result +} + +func getServicePorts(spec corev1.ServiceSpec) []string { + result := []string{} + for _, servicePort := range spec.Ports { + result = append(result, strconv.Itoa(int(servicePort.Port))) + } + return result +} diff --git a/pkg/polymorphichelpers/portsforobject_test.go b/pkg/polymorphichelpers/portsforobject_test.go new file mode 100644 index 000000000..da5f21e6c --- /dev/null +++ b/pkg/polymorphichelpers/portsforobject_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "testing" + + "reflect" + + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestPortsForObject(t *testing.T) { + tests := []struct { + object runtime.Object + expectErr bool + }{ + { + object: &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + ContainerPort: 101, + }, + }, + }, + }, + }, + }, + }, + { + object: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 101, + }, + }, + }, + }, + }, + { + object: &corev1.ReplicationController{ + Spec: corev1.ReplicationControllerSpec{ + Template: &corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + ContainerPort: 101, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + object: &extensionsv1beta1.Deployment{ + Spec: extensionsv1beta1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + ContainerPort: 101, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + object: &extensionsv1beta1.ReplicaSet{ + Spec: extensionsv1beta1.ReplicaSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + ContainerPort: 101, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + object: &corev1.Node{}, + expectErr: true, + }, + } + expectedPorts := []string{"101"} + + for _, test := range tests { + actual, err := portsForObject(test.object) + if test.expectErr { + if err == nil { + t.Error("unexpected non-error") + } + continue + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + continue + } + if !reflect.DeepEqual(actual, expectedPorts) { + t.Errorf("expected ports %v, but got %v", expectedPorts, actual) + } + } +} diff --git a/pkg/polymorphichelpers/protocolsforobject.go b/pkg/polymorphichelpers/protocolsforobject.go new file mode 100644 index 000000000..2e5e5a208 --- /dev/null +++ b/pkg/polymorphichelpers/protocolsforobject.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "fmt" + "strconv" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func protocolsForObject(object runtime.Object) (map[string]string, error) { + // TODO: replace with a swagger schema based approach (identify pod selector via schema introspection) + switch t := object.(type) { + case *corev1.ReplicationController: + return getProtocols(t.Spec.Template.Spec), nil + + case *corev1.Pod: + return getProtocols(t.Spec), nil + + case *corev1.Service: + return getServiceProtocols(t.Spec), nil + + case *extensionsv1beta1.Deployment: + return getProtocols(t.Spec.Template.Spec), nil + case *appsv1.Deployment: + return getProtocols(t.Spec.Template.Spec), nil + case *appsv1beta2.Deployment: + return getProtocols(t.Spec.Template.Spec), nil + case *appsv1beta1.Deployment: + return getProtocols(t.Spec.Template.Spec), nil + + case *extensionsv1beta1.ReplicaSet: + return getProtocols(t.Spec.Template.Spec), nil + case *appsv1.ReplicaSet: + return getProtocols(t.Spec.Template.Spec), nil + case *appsv1beta2.ReplicaSet: + return getProtocols(t.Spec.Template.Spec), nil + + default: + return nil, fmt.Errorf("cannot extract protocols from %T", object) + } +} + +func getProtocols(spec corev1.PodSpec) map[string]string { + result := make(map[string]string) + for _, container := range spec.Containers { + for _, port := range container.Ports { + // Empty protocol must be defaulted (TCP) + if len(port.Protocol) == 0 { + port.Protocol = corev1.ProtocolTCP + } + result[strconv.Itoa(int(port.ContainerPort))] = string(port.Protocol) + } + } + return result +} + +// Extracts the protocols exposed by a service from the given service spec. +func getServiceProtocols(spec corev1.ServiceSpec) map[string]string { + result := make(map[string]string) + for _, servicePort := range spec.Ports { + // Empty protocol must be defaulted (TCP) + if len(servicePort.Protocol) == 0 { + servicePort.Protocol = corev1.ProtocolTCP + } + result[strconv.Itoa(int(servicePort.Port))] = string(servicePort.Protocol) + } + return result +} diff --git a/pkg/polymorphichelpers/protocolsforobject_test.go b/pkg/polymorphichelpers/protocolsforobject_test.go new file mode 100644 index 000000000..d31da0614 --- /dev/null +++ b/pkg/polymorphichelpers/protocolsforobject_test.go @@ -0,0 +1,173 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "testing" + + "reflect" + + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestProtocolsForObject(t *testing.T) { + tests := []struct { + object runtime.Object + expectErr bool + }{ + { + object: &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + ContainerPort: 101, + Protocol: "TCP", + }, + }, + }, + }, + }, + }, + }, + // No protocol--should default to TCP. + { + object: &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + ContainerPort: 101, + }, + }, + }, + }, + }, + }, + }, + { + object: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 101, + Protocol: "TCP", + }, + }, + }, + }, + }, + // No protocol for service port--default to TCP + { + object: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 101, + }, + }, + }, + }, + }, + { + object: &corev1.ReplicationController{ + Spec: corev1.ReplicationControllerSpec{ + Template: &corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + ContainerPort: 101, + Protocol: "TCP", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + object: &extensionsv1beta1.Deployment{ + Spec: extensionsv1beta1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + ContainerPort: 101, + Protocol: "TCP", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + object: &extensionsv1beta1.ReplicaSet{ + Spec: extensionsv1beta1.ReplicaSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Ports: []corev1.ContainerPort{ + { + ContainerPort: 101, + Protocol: "TCP", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + object: &corev1.Node{}, + expectErr: true, + }, + } + expectedPorts := map[string]string{"101": "TCP"} + + for _, test := range tests { + actual, err := protocolsForObject(test.object) + if test.expectErr { + if err == nil { + t.Error("unexpected non-error") + } + continue + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + continue + } + if !reflect.DeepEqual(actual, expectedPorts) { + t.Errorf("expected ports %v, but got %v", expectedPorts, actual) + } + } +} diff --git a/pkg/polymorphichelpers/rollback.go b/pkg/polymorphichelpers/rollback.go new file mode 100644 index 000000000..8127ceeca --- /dev/null +++ b/pkg/polymorphichelpers/rollback.go @@ -0,0 +1,486 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "bytes" + "fmt" + "sort" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/client-go/kubernetes" + "k8s.io/kubectl/pkg/apps" + "k8s.io/kubectl/pkg/scheme" + deploymentutil "k8s.io/kubectl/pkg/util/deployment" +) + +const ( + rollbackSuccess = "rolled back" + rollbackSkipped = "skipped rollback" +) + +// Rollbacker provides an interface for resources that can be rolled back. +type Rollbacker interface { + Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) +} + +type RollbackVisitor struct { + clientset kubernetes.Interface + result Rollbacker +} + +func (v *RollbackVisitor) VisitDeployment(elem apps.GroupKindElement) { + v.result = &DeploymentRollbacker{v.clientset} +} + +func (v *RollbackVisitor) VisitStatefulSet(kind apps.GroupKindElement) { + v.result = &StatefulSetRollbacker{v.clientset} +} + +func (v *RollbackVisitor) VisitDaemonSet(kind apps.GroupKindElement) { + v.result = &DaemonSetRollbacker{v.clientset} +} + +func (v *RollbackVisitor) VisitJob(kind apps.GroupKindElement) {} +func (v *RollbackVisitor) VisitPod(kind apps.GroupKindElement) {} +func (v *RollbackVisitor) VisitReplicaSet(kind apps.GroupKindElement) {} +func (v *RollbackVisitor) VisitReplicationController(kind apps.GroupKindElement) {} +func (v *RollbackVisitor) VisitCronJob(kind apps.GroupKindElement) {} + +// RollbackerFor returns an implementation of Rollbacker interface for the given schema kind +func RollbackerFor(kind schema.GroupKind, c kubernetes.Interface) (Rollbacker, error) { + elem := apps.GroupKindElement(kind) + visitor := &RollbackVisitor{ + clientset: c, + } + + err := elem.Accept(visitor) + + if err != nil { + return nil, fmt.Errorf("error retrieving rollbacker for %q, %v", kind.String(), err) + } + + if visitor.result == nil { + return nil, fmt.Errorf("no rollbacker has been implemented for %q", kind) + } + + return visitor.result, nil +} + +type DeploymentRollbacker struct { + c kubernetes.Interface +} + +func (r *DeploymentRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) { + if toRevision < 0 { + return "", revisionNotFoundErr(toRevision) + } + accessor, err := meta.Accessor(obj) + if err != nil { + return "", fmt.Errorf("failed to create accessor for kind %v: %s", obj.GetObjectKind(), err.Error()) + } + name := accessor.GetName() + namespace := accessor.GetNamespace() + + // TODO: Fix this after kubectl has been removed from core. It is not possible to convert the runtime.Object + // to the external appsv1 Deployment without round-tripping through an internal version of Deployment. We're + // currently getting rid of all internal versions of resources. So we specifically request the appsv1 version + // here. This follows the same pattern as for DaemonSet and StatefulSet. + deployment, err := r.c.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return "", fmt.Errorf("failed to retrieve Deployment %s: %v", name, err) + } + + rsForRevision, err := deploymentRevision(deployment, r.c, toRevision) + if err != nil { + return "", err + } + if dryRun { + return printTemplate(&rsForRevision.Spec.Template) + } + if deployment.Spec.Paused { + return "", fmt.Errorf("you cannot rollback a paused deployment; resume it first with 'kubectl rollout resume deployment/%s' and try again", name) + } + + // Skip if the revision already matches current Deployment + if equalIgnoreHash(&rsForRevision.Spec.Template, &deployment.Spec.Template) { + return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil + } + + // remove hash label before patching back into the deployment + delete(rsForRevision.Spec.Template.Labels, appsv1.DefaultDeploymentUniqueLabelKey) + + // compute deployment annotations + annotations := map[string]string{} + for k := range annotationsToSkip { + if v, ok := deployment.Annotations[k]; ok { + annotations[k] = v + } + } + for k, v := range rsForRevision.Annotations { + if !annotationsToSkip[k] { + annotations[k] = v + } + } + + // make patch to restore + patchType, patch, err := getDeploymentPatch(&rsForRevision.Spec.Template, annotations) + if err != nil { + return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err) + } + + // Restore revision + if _, err = r.c.AppsV1().Deployments(namespace).Patch(name, patchType, patch); err != nil { + return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err) + } + return rollbackSuccess, nil +} + +// equalIgnoreHash returns true if two given podTemplateSpec are equal, ignoring the diff in value of Labels[pod-template-hash] +// We ignore pod-template-hash because: +// 1. The hash result would be different upon podTemplateSpec API changes +// (e.g. the addition of a new field will cause the hash code to change) +// 2. The deployment template won't have hash labels +func equalIgnoreHash(template1, template2 *corev1.PodTemplateSpec) bool { + t1Copy := template1.DeepCopy() + t2Copy := template2.DeepCopy() + // Remove hash labels from template.Labels before comparing + delete(t1Copy.Labels, appsv1.DefaultDeploymentUniqueLabelKey) + delete(t2Copy.Labels, appsv1.DefaultDeploymentUniqueLabelKey) + return apiequality.Semantic.DeepEqual(t1Copy, t2Copy) +} + +// annotationsToSkip lists the annotations that should be preserved from the deployment and not +// copied from the replicaset when rolling a deployment back +var annotationsToSkip = map[string]bool{ + corev1.LastAppliedConfigAnnotation: true, + deploymentutil.RevisionAnnotation: true, + deploymentutil.RevisionHistoryAnnotation: true, + deploymentutil.DesiredReplicasAnnotation: true, + deploymentutil.MaxReplicasAnnotation: true, + appsv1.DeprecatedRollbackTo: true, +} + +// getPatch returns a patch that can be applied to restore a Deployment to a +// previous version. If the returned error is nil the patch is valid. +func getDeploymentPatch(podTemplate *corev1.PodTemplateSpec, annotations map[string]string) (types.PatchType, []byte, error) { + // Create a patch of the Deployment that replaces spec.template + patch, err := json.Marshal([]interface{}{ + map[string]interface{}{ + "op": "replace", + "path": "/spec/template", + "value": podTemplate, + }, + map[string]interface{}{ + "op": "replace", + "path": "/metadata/annotations", + "value": annotations, + }, + }) + return types.JSONPatchType, patch, err +} + +func deploymentRevision(deployment *appsv1.Deployment, c kubernetes.Interface, toRevision int64) (revision *appsv1.ReplicaSet, err error) { + + _, allOldRSs, newRS, err := deploymentutil.GetAllReplicaSets(deployment, c.AppsV1()) + if err != nil { + return nil, fmt.Errorf("failed to retrieve replica sets from deployment %s: %v", deployment.Name, err) + } + allRSs := allOldRSs + if newRS != nil { + allRSs = append(allRSs, newRS) + } + + var ( + latestReplicaSet *appsv1.ReplicaSet + latestRevision = int64(-1) + previousReplicaSet *appsv1.ReplicaSet + previousRevision = int64(-1) + ) + for _, rs := range allRSs { + if v, err := deploymentutil.Revision(rs); err == nil { + if toRevision == 0 { + if latestRevision < v { + // newest one we've seen so far + previousRevision = latestRevision + previousReplicaSet = latestReplicaSet + latestRevision = v + latestReplicaSet = rs + } else if previousRevision < v { + // second newest one we've seen so far + previousRevision = v + previousReplicaSet = rs + } + } else if toRevision == v { + return rs, nil + } + } + } + + if toRevision > 0 { + return nil, revisionNotFoundErr(toRevision) + } + + if previousReplicaSet == nil { + return nil, fmt.Errorf("no rollout history found for deployment %q", deployment.Name) + } + return previousReplicaSet, nil +} + +type DaemonSetRollbacker struct { + c kubernetes.Interface +} + +func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) { + if toRevision < 0 { + return "", revisionNotFoundErr(toRevision) + } + accessor, err := meta.Accessor(obj) + if err != nil { + return "", fmt.Errorf("failed to create accessor for kind %v: %s", obj.GetObjectKind(), err.Error()) + } + ds, history, err := daemonSetHistory(r.c.AppsV1(), accessor.GetNamespace(), accessor.GetName()) + if err != nil { + return "", err + } + if toRevision == 0 && len(history) <= 1 { + return "", fmt.Errorf("no last revision to roll back to") + } + + toHistory := findHistory(toRevision, history) + if toHistory == nil { + return "", revisionNotFoundErr(toRevision) + } + + if dryRun { + appliedDS, err := applyDaemonSetHistory(ds, toHistory) + if err != nil { + return "", err + } + return printPodTemplate(&appliedDS.Spec.Template) + } + + // Skip if the revision already matches current DaemonSet + done, err := daemonSetMatch(ds, toHistory) + if err != nil { + return "", err + } + if done { + return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil + } + + // Restore revision + if _, err = r.c.AppsV1().DaemonSets(accessor.GetNamespace()).Patch(accessor.GetName(), types.StrategicMergePatchType, toHistory.Data.Raw); err != nil { + return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err) + } + + return rollbackSuccess, nil +} + +// daemonMatch check if the given DaemonSet's template matches the template stored in the given history. +func daemonSetMatch(ds *appsv1.DaemonSet, history *appsv1.ControllerRevision) (bool, error) { + patch, err := getDaemonSetPatch(ds) + if err != nil { + return false, err + } + return bytes.Equal(patch, history.Data.Raw), nil +} + +// getPatch returns a strategic merge patch that can be applied to restore a Daemonset to a +// previous version. If the returned error is nil the patch is valid. The current state that we save is just the +// PodSpecTemplate. We can modify this later to encompass more state (or less) and remain compatible with previously +// recorded patches. +func getDaemonSetPatch(ds *appsv1.DaemonSet) ([]byte, error) { + dsBytes, err := json.Marshal(ds) + if err != nil { + return nil, err + } + var raw map[string]interface{} + err = json.Unmarshal(dsBytes, &raw) + if err != nil { + return nil, err + } + objCopy := make(map[string]interface{}) + specCopy := make(map[string]interface{}) + + // Create a patch of the DaemonSet that replaces spec.template + spec := raw["spec"].(map[string]interface{}) + template := spec["template"].(map[string]interface{}) + specCopy["template"] = template + template["$patch"] = "replace" + objCopy["spec"] = specCopy + patch, err := json.Marshal(objCopy) + return patch, err +} + +type StatefulSetRollbacker struct { + c kubernetes.Interface +} + +// toRevision is a non-negative integer, with 0 being reserved to indicate rolling back to previous configuration +func (r *StatefulSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) { + if toRevision < 0 { + return "", revisionNotFoundErr(toRevision) + } + accessor, err := meta.Accessor(obj) + if err != nil { + return "", fmt.Errorf("failed to create accessor for kind %v: %s", obj.GetObjectKind(), err.Error()) + } + sts, history, err := statefulSetHistory(r.c.AppsV1(), accessor.GetNamespace(), accessor.GetName()) + if err != nil { + return "", err + } + if toRevision == 0 && len(history) <= 1 { + return "", fmt.Errorf("no last revision to roll back to") + } + + toHistory := findHistory(toRevision, history) + if toHistory == nil { + return "", revisionNotFoundErr(toRevision) + } + + if dryRun { + appliedSS, err := applyRevision(sts, toHistory) + if err != nil { + return "", err + } + return printPodTemplate(&appliedSS.Spec.Template) + } + + // Skip if the revision already matches current StatefulSet + done, err := statefulsetMatch(sts, toHistory) + if err != nil { + return "", err + } + if done { + return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil + } + + // Restore revision + if _, err = r.c.AppsV1().StatefulSets(sts.Namespace).Patch(sts.Name, types.StrategicMergePatchType, toHistory.Data.Raw); err != nil { + return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err) + } + + return rollbackSuccess, nil +} + +var appsCodec = scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion) + +// applyRevision returns a new StatefulSet constructed by restoring the state in revision to set. If the returned error +// is nil, the returned StatefulSet is valid. +func applyRevision(set *appsv1.StatefulSet, revision *appsv1.ControllerRevision) (*appsv1.StatefulSet, error) { + clone := set.DeepCopy() + patched, err := strategicpatch.StrategicMergePatch([]byte(runtime.EncodeOrDie(appsCodec, clone)), revision.Data.Raw, clone) + if err != nil { + return nil, err + } + err = json.Unmarshal(patched, clone) + if err != nil { + return nil, err + } + return clone, nil +} + +// statefulsetMatch check if the given StatefulSet's template matches the template stored in the given history. +func statefulsetMatch(ss *appsv1.StatefulSet, history *appsv1.ControllerRevision) (bool, error) { + patch, err := getStatefulSetPatch(ss) + if err != nil { + return false, err + } + return bytes.Equal(patch, history.Data.Raw), nil +} + +// getStatefulSetPatch returns a strategic merge patch that can be applied to restore a StatefulSet to a +// previous version. If the returned error is nil the patch is valid. The current state that we save is just the +// PodSpecTemplate. We can modify this later to encompass more state (or less) and remain compatible with previously +// recorded patches. +func getStatefulSetPatch(set *appsv1.StatefulSet) ([]byte, error) { + str, err := runtime.Encode(appsCodec, set) + if err != nil { + return nil, err + } + var raw map[string]interface{} + if err := json.Unmarshal([]byte(str), &raw); err != nil { + return nil, err + } + objCopy := make(map[string]interface{}) + specCopy := make(map[string]interface{}) + spec := raw["spec"].(map[string]interface{}) + template := spec["template"].(map[string]interface{}) + specCopy["template"] = template + template["$patch"] = "replace" + objCopy["spec"] = specCopy + patch, err := json.Marshal(objCopy) + return patch, err +} + +// findHistory returns a controllerrevision of a specific revision from the given controllerrevisions. +// It returns nil if no such controllerrevision exists. +// If toRevision is 0, the last previously used history is returned. +func findHistory(toRevision int64, allHistory []*appsv1.ControllerRevision) *appsv1.ControllerRevision { + if toRevision == 0 && len(allHistory) <= 1 { + return nil + } + + // Find the history to rollback to + var toHistory *appsv1.ControllerRevision + if toRevision == 0 { + // If toRevision == 0, find the latest revision (2nd max) + sort.Sort(historiesByRevision(allHistory)) + toHistory = allHistory[len(allHistory)-2] + } else { + for _, h := range allHistory { + if h.Revision == toRevision { + // If toRevision != 0, find the history with matching revision + return h + } + } + } + + return toHistory +} + +// printPodTemplate converts a given pod template into a human-readable string. +func printPodTemplate(specTemplate *corev1.PodTemplateSpec) (string, error) { + podSpec, err := printTemplate(specTemplate) + if err != nil { + return "", err + } + return fmt.Sprintf("will roll back to %s", podSpec), nil +} + +func revisionNotFoundErr(r int64) error { + return fmt.Errorf("unable to find specified revision %v in history", r) +} + +// TODO: copied from daemon controller, should extract to a library +type historiesByRevision []*appsv1.ControllerRevision + +func (h historiesByRevision) Len() int { return len(h) } +func (h historiesByRevision) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h historiesByRevision) Less(i, j int) bool { + return h[i].Revision < h[j].Revision +} diff --git a/pkg/polymorphichelpers/rollback_test.go b/pkg/polymorphichelpers/rollback_test.go new file mode 100644 index 000000000..673e8847b --- /dev/null +++ b/pkg/polymorphichelpers/rollback_test.go @@ -0,0 +1,67 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" +) + +var rollbacktests = map[schema.GroupKind]reflect.Type{ + {Group: "apps", Kind: "DaemonSet"}: reflect.TypeOf(&DaemonSetRollbacker{}), + {Group: "apps", Kind: "StatefulSet"}: reflect.TypeOf(&StatefulSetRollbacker{}), + {Group: "apps", Kind: "Deployment"}: reflect.TypeOf(&DeploymentRollbacker{}), +} + +func TestRollbackerFor(t *testing.T) { + fakeClientset := &fake.Clientset{} + + for kind, expectedType := range rollbacktests { + result, err := RollbackerFor(kind, fakeClientset) + if err != nil { + t.Fatalf("error getting Rollbacker for a %v: %v", kind.String(), err) + } + + if reflect.TypeOf(result) != expectedType { + t.Fatalf("unexpected output type (%v was expected but got %v)", expectedType, reflect.TypeOf(result)) + } + } +} + +func TestGetDeploymentPatch(t *testing.T) { + patchType, patchBytes, err := getDeploymentPatch(&corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Image: "foo"}}}}, map[string]string{"a": "true"}) + if err != nil { + t.Error(err) + } + if patchType != types.JSONPatchType { + t.Errorf("expected strategic merge patch, got %v", patchType) + } + expectedPatch := `[` + + `{"op":"replace","path":"/spec/template","value":{"metadata":{"creationTimestamp":null},"spec":{"containers":[{"name":"","image":"foo","resources":{}}]}}},` + + `{"op":"replace","path":"/metadata/annotations","value":{"a":"true"}}` + + `]` + if string(patchBytes) != expectedPatch { + t.Errorf("expected:\n%s\ngot\n%s", expectedPatch, string(patchBytes)) + } +} diff --git a/pkg/polymorphichelpers/rollbacker.go b/pkg/polymorphichelpers/rollbacker.go new file mode 100644 index 000000000..f02223690 --- /dev/null +++ b/pkg/polymorphichelpers/rollbacker.go @@ -0,0 +1,37 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" +) + +// Returns a Rollbacker for changing the rollback version of the specified RESTMapping type or an error +func rollbacker(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (Rollbacker, error) { + clientConfig, err := restClientGetter.ToRESTConfig() + if err != nil { + return nil, err + } + external, err := kubernetes.NewForConfig(clientConfig) + if err != nil { + return nil, err + } + + return RollbackerFor(mapping.GroupVersionKind.GroupKind(), external) +} diff --git a/pkg/polymorphichelpers/rollout_status.go b/pkg/polymorphichelpers/rollout_status.go new file mode 100644 index 000000000..6c90811f4 --- /dev/null +++ b/pkg/polymorphichelpers/rollout_status.go @@ -0,0 +1,153 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/scheme" + deploymentutil "k8s.io/kubectl/pkg/util/deployment" +) + +// StatusViewer provides an interface for resources that have rollout status. +type StatusViewer interface { + Status(obj runtime.Unstructured, revision int64) (string, bool, error) +} + +// StatusViewerFor returns a StatusViewer for the resource specified by kind. +func StatusViewerFor(kind schema.GroupKind) (StatusViewer, error) { + switch kind { + case extensionsv1beta1.SchemeGroupVersion.WithKind("Deployment").GroupKind(), + appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind(): + return &DeploymentStatusViewer{}, nil + case extensionsv1beta1.SchemeGroupVersion.WithKind("DaemonSet").GroupKind(), + appsv1.SchemeGroupVersion.WithKind("DaemonSet").GroupKind(): + return &DaemonSetStatusViewer{}, nil + case appsv1.SchemeGroupVersion.WithKind("StatefulSet").GroupKind(): + return &StatefulSetStatusViewer{}, nil + } + return nil, fmt.Errorf("no status viewer has been implemented for %v", kind) +} + +// DeploymentStatusViewer implements the StatusViewer interface. +type DeploymentStatusViewer struct{} + +// DaemonSetStatusViewer implements the StatusViewer interface. +type DaemonSetStatusViewer struct{} + +// StatefulSetStatusViewer implements the StatusViewer interface. +type StatefulSetStatusViewer struct{} + +// Status returns a message describing deployment status, and a bool value indicating if the status is considered done. +func (s *DeploymentStatusViewer) Status(obj runtime.Unstructured, revision int64) (string, bool, error) { + deployment := &appsv1.Deployment{} + err := scheme.Scheme.Convert(obj, deployment, nil) + if err != nil { + return "", false, fmt.Errorf("failed to convert %T to %T: %v", obj, deployment, err) + } + + if revision > 0 { + deploymentRev, err := deploymentutil.Revision(deployment) + if err != nil { + return "", false, fmt.Errorf("cannot get the revision of deployment %q: %v", deployment.Name, err) + } + if revision != deploymentRev { + return "", false, fmt.Errorf("desired revision (%d) is different from the running revision (%d)", revision, deploymentRev) + } + } + if deployment.Generation <= deployment.Status.ObservedGeneration { + cond := deploymentutil.GetDeploymentCondition(deployment.Status, appsv1.DeploymentProgressing) + if cond != nil && cond.Reason == deploymentutil.TimedOutReason { + return "", false, fmt.Errorf("deployment %q exceeded its progress deadline", deployment.Name) + } + if deployment.Spec.Replicas != nil && deployment.Status.UpdatedReplicas < *deployment.Spec.Replicas { + return fmt.Sprintf("Waiting for deployment %q rollout to finish: %d out of %d new replicas have been updated...\n", deployment.Name, deployment.Status.UpdatedReplicas, *deployment.Spec.Replicas), false, nil + } + if deployment.Status.Replicas > deployment.Status.UpdatedReplicas { + return fmt.Sprintf("Waiting for deployment %q rollout to finish: %d old replicas are pending termination...\n", deployment.Name, deployment.Status.Replicas-deployment.Status.UpdatedReplicas), false, nil + } + if deployment.Status.AvailableReplicas < deployment.Status.UpdatedReplicas { + return fmt.Sprintf("Waiting for deployment %q rollout to finish: %d of %d updated replicas are available...\n", deployment.Name, deployment.Status.AvailableReplicas, deployment.Status.UpdatedReplicas), false, nil + } + return fmt.Sprintf("deployment %q successfully rolled out\n", deployment.Name), true, nil + } + return fmt.Sprintf("Waiting for deployment spec update to be observed...\n"), false, nil +} + +// Status returns a message describing daemon set status, and a bool value indicating if the status is considered done. +func (s *DaemonSetStatusViewer) Status(obj runtime.Unstructured, revision int64) (string, bool, error) { + //ignoring revision as DaemonSets does not have history yet + + daemon := &appsv1.DaemonSet{} + err := scheme.Scheme.Convert(obj, daemon, nil) + if err != nil { + return "", false, fmt.Errorf("failed to convert %T to %T: %v", obj, daemon, err) + } + + if daemon.Spec.UpdateStrategy.Type != appsv1.RollingUpdateDaemonSetStrategyType { + return "", true, fmt.Errorf("rollout status is only available for %s strategy type", appsv1.RollingUpdateStatefulSetStrategyType) + } + if daemon.Generation <= daemon.Status.ObservedGeneration { + if daemon.Status.UpdatedNumberScheduled < daemon.Status.DesiredNumberScheduled { + return fmt.Sprintf("Waiting for daemon set %q rollout to finish: %d out of %d new pods have been updated...\n", daemon.Name, daemon.Status.UpdatedNumberScheduled, daemon.Status.DesiredNumberScheduled), false, nil + } + if daemon.Status.NumberAvailable < daemon.Status.DesiredNumberScheduled { + return fmt.Sprintf("Waiting for daemon set %q rollout to finish: %d of %d updated pods are available...\n", daemon.Name, daemon.Status.NumberAvailable, daemon.Status.DesiredNumberScheduled), false, nil + } + return fmt.Sprintf("daemon set %q successfully rolled out\n", daemon.Name), true, nil + } + return fmt.Sprintf("Waiting for daemon set spec update to be observed...\n"), false, nil +} + +// Status returns a message describing statefulset status, and a bool value indicating if the status is considered done. +func (s *StatefulSetStatusViewer) Status(obj runtime.Unstructured, revision int64) (string, bool, error) { + sts := &appsv1.StatefulSet{} + err := scheme.Scheme.Convert(obj, sts, nil) + if err != nil { + return "", false, fmt.Errorf("failed to convert %T to %T: %v", obj, sts, err) + } + + if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType { + return "", true, fmt.Errorf("rollout status is only available for %s strategy type", appsv1.RollingUpdateStatefulSetStrategyType) + } + if sts.Status.ObservedGeneration == 0 || sts.Generation > sts.Status.ObservedGeneration { + return "Waiting for statefulset spec update to be observed...\n", false, nil + } + if sts.Spec.Replicas != nil && sts.Status.ReadyReplicas < *sts.Spec.Replicas { + return fmt.Sprintf("Waiting for %d pods to be ready...\n", *sts.Spec.Replicas-sts.Status.ReadyReplicas), false, nil + } + if sts.Spec.UpdateStrategy.Type == appsv1.RollingUpdateStatefulSetStrategyType && sts.Spec.UpdateStrategy.RollingUpdate != nil { + if sts.Spec.Replicas != nil && sts.Spec.UpdateStrategy.RollingUpdate.Partition != nil { + if sts.Status.UpdatedReplicas < (*sts.Spec.Replicas - *sts.Spec.UpdateStrategy.RollingUpdate.Partition) { + return fmt.Sprintf("Waiting for partitioned roll out to finish: %d out of %d new pods have been updated...\n", + sts.Status.UpdatedReplicas, *sts.Spec.Replicas-*sts.Spec.UpdateStrategy.RollingUpdate.Partition), false, nil + } + } + return fmt.Sprintf("partitioned roll out complete: %d new pods have been updated...\n", + sts.Status.UpdatedReplicas), true, nil + } + if sts.Status.UpdateRevision != sts.Status.CurrentRevision { + return fmt.Sprintf("waiting for statefulset rolling update to complete %d pods at revision %s...\n", + sts.Status.UpdatedReplicas, sts.Status.UpdateRevision), false, nil + } + return fmt.Sprintf("statefulset rolling update complete %d pods at revision %s...\n", sts.Status.CurrentReplicas, sts.Status.CurrentRevision), true, nil + +} diff --git a/pkg/polymorphichelpers/rollout_status_test.go b/pkg/polymorphichelpers/rollout_status_test.go new file mode 100644 index 000000000..de5990c47 --- /dev/null +++ b/pkg/polymorphichelpers/rollout_status_test.go @@ -0,0 +1,469 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "fmt" + "testing" + + apps "k8s.io/api/apps/v1" + api "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/kubectl/pkg/scheme" +) + +func TestDeploymentStatusViewerStatus(t *testing.T) { + tests := []struct { + name string + generation int64 + specReplicas int32 + status apps.DeploymentStatus + msg string + done bool + }{ + { + name: "test1", + generation: 0, + specReplicas: 1, + status: apps.DeploymentStatus{ + ObservedGeneration: 1, + Replicas: 1, + UpdatedReplicas: 0, + AvailableReplicas: 1, + UnavailableReplicas: 0, + }, + + msg: "Waiting for deployment \"foo\" rollout to finish: 0 out of 1 new replicas have been updated...\n", + done: false, + }, + { + name: "test2", + generation: 1, + specReplicas: 1, + status: apps.DeploymentStatus{ + ObservedGeneration: 1, + Replicas: 2, + UpdatedReplicas: 1, + AvailableReplicas: 2, + UnavailableReplicas: 0, + }, + + msg: "Waiting for deployment \"foo\" rollout to finish: 1 old replicas are pending termination...\n", + done: false, + }, + { + name: "test3", + generation: 1, + specReplicas: 2, + status: apps.DeploymentStatus{ + ObservedGeneration: 1, + Replicas: 2, + UpdatedReplicas: 2, + AvailableReplicas: 1, + UnavailableReplicas: 1, + }, + + msg: "Waiting for deployment \"foo\" rollout to finish: 1 of 2 updated replicas are available...\n", + done: false, + }, + { + name: "test4", + generation: 1, + specReplicas: 2, + status: apps.DeploymentStatus{ + ObservedGeneration: 1, + Replicas: 2, + UpdatedReplicas: 2, + AvailableReplicas: 2, + UnavailableReplicas: 0, + }, + + msg: "deployment \"foo\" successfully rolled out\n", + done: true, + }, + { + name: "test5", + generation: 2, + specReplicas: 2, + status: apps.DeploymentStatus{ + ObservedGeneration: 1, + Replicas: 2, + UpdatedReplicas: 2, + AvailableReplicas: 2, + UnavailableReplicas: 0, + }, + + msg: "Waiting for deployment spec update to be observed...\n", + done: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + d := &apps.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "bar", + Name: "foo", + UID: "8764ae47-9092-11e4-8393-42010af018ff", + Generation: test.generation, + }, + Spec: apps.DeploymentSpec{ + Replicas: &test.specReplicas, + }, + Status: test.status, + } + unstructuredD := &unstructured.Unstructured{} + err := scheme.Scheme.Convert(d, unstructuredD, nil) + if err != nil { + t.Fatal(err) + } + + dsv := &DeploymentStatusViewer{} + msg, done, err := dsv.Status(unstructuredD, 0) + if err != nil { + t.Fatalf("DeploymentStatusViewer.Status(): %v", err) + } + if done != test.done || msg != test.msg { + t.Errorf("DeploymentStatusViewer.Status() for deployment with generation %d, %d replicas specified, and status %+v returned %q, %t, want %q, %t", + test.generation, + test.specReplicas, + test.status, + msg, + done, + test.msg, + test.done, + ) + } + }) + } +} + +func TestDaemonSetStatusViewerStatus(t *testing.T) { + tests := []struct { + name string + generation int64 + status apps.DaemonSetStatus + msg string + done bool + }{ + { + name: "test1", + generation: 0, + status: apps.DaemonSetStatus{ + ObservedGeneration: 1, + UpdatedNumberScheduled: 0, + DesiredNumberScheduled: 1, + NumberAvailable: 0, + }, + + msg: "Waiting for daemon set \"foo\" rollout to finish: 0 out of 1 new pods have been updated...\n", + done: false, + }, + { + name: "test2", + generation: 1, + status: apps.DaemonSetStatus{ + ObservedGeneration: 1, + UpdatedNumberScheduled: 2, + DesiredNumberScheduled: 2, + NumberAvailable: 1, + }, + + msg: "Waiting for daemon set \"foo\" rollout to finish: 1 of 2 updated pods are available...\n", + done: false, + }, + { + name: "test3", + generation: 1, + status: apps.DaemonSetStatus{ + ObservedGeneration: 1, + UpdatedNumberScheduled: 2, + DesiredNumberScheduled: 2, + NumberAvailable: 2, + }, + + msg: "daemon set \"foo\" successfully rolled out\n", + done: true, + }, + { + name: "test4", + generation: 2, + status: apps.DaemonSetStatus{ + ObservedGeneration: 1, + UpdatedNumberScheduled: 2, + DesiredNumberScheduled: 2, + NumberAvailable: 2, + }, + + msg: "Waiting for daemon set spec update to be observed...\n", + done: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + d := &apps.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "bar", + Name: "foo", + UID: "8764ae47-9092-11e4-8393-42010af018ff", + Generation: test.generation, + }, + Spec: apps.DaemonSetSpec{ + UpdateStrategy: apps.DaemonSetUpdateStrategy{ + Type: apps.RollingUpdateDaemonSetStrategyType, + }, + }, + Status: test.status, + } + + unstructuredD := &unstructured.Unstructured{} + err := scheme.Scheme.Convert(d, unstructuredD, nil) + if err != nil { + t.Fatal(err) + } + + dsv := &DaemonSetStatusViewer{} + msg, done, err := dsv.Status(unstructuredD, 0) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if done != test.done || msg != test.msg { + t.Errorf("daemon set with generation %d, %d pods specified, and status:\n%+v\nreturned:\n%q, %t\nwant:\n%q, %t", + test.generation, + d.Status.DesiredNumberScheduled, + test.status, + msg, + done, + test.msg, + test.done, + ) + } + }) + } +} + +func TestStatefulSetStatusViewerStatus(t *testing.T) { + tests := []struct { + name string + generation int64 + strategy apps.StatefulSetUpdateStrategy + status apps.StatefulSetStatus + msg string + done bool + err bool + }{ + { + name: "on delete returns an error", + generation: 1, + strategy: apps.StatefulSetUpdateStrategy{Type: apps.OnDeleteStatefulSetStrategyType}, + status: apps.StatefulSetStatus{ + ObservedGeneration: 1, + Replicas: 0, + ReadyReplicas: 1, + CurrentReplicas: 0, + UpdatedReplicas: 0, + }, + + msg: "", + done: true, + err: true, + }, + { + name: "unobserved update is not complete", + generation: 2, + strategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType}, + status: apps.StatefulSetStatus{ + ObservedGeneration: 1, + Replicas: 3, + ReadyReplicas: 3, + CurrentReplicas: 3, + UpdatedReplicas: 0, + }, + + msg: "Waiting for statefulset spec update to be observed...\n", + done: false, + err: false, + }, + { + name: "if all pods are not ready the update is not complete", + generation: 1, + strategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType}, + status: apps.StatefulSetStatus{ + ObservedGeneration: 2, + Replicas: 3, + ReadyReplicas: 2, + CurrentReplicas: 3, + UpdatedReplicas: 0, + }, + + msg: fmt.Sprintf("Waiting for %d pods to be ready...\n", 1), + done: false, + err: false, + }, + { + name: "partition update completes when all replicas above the partition are updated", + generation: 1, + strategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType, + RollingUpdate: func() *apps.RollingUpdateStatefulSetStrategy { + partition := int32(2) + return &apps.RollingUpdateStatefulSetStrategy{Partition: &partition} + }()}, + status: apps.StatefulSetStatus{ + ObservedGeneration: 2, + Replicas: 3, + ReadyReplicas: 3, + CurrentReplicas: 2, + UpdatedReplicas: 1, + }, + + msg: fmt.Sprintf("partitioned roll out complete: %d new pods have been updated...\n", 1), + done: true, + err: false, + }, + { + name: "partition update is in progress if all pods above the partition have not been updated", + generation: 1, + strategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType, + RollingUpdate: func() *apps.RollingUpdateStatefulSetStrategy { + partition := int32(2) + return &apps.RollingUpdateStatefulSetStrategy{Partition: &partition} + }()}, + status: apps.StatefulSetStatus{ + ObservedGeneration: 2, + Replicas: 3, + ReadyReplicas: 3, + CurrentReplicas: 3, + UpdatedReplicas: 0, + }, + + msg: fmt.Sprintf("Waiting for partitioned roll out to finish: %d out of %d new pods have been updated...\n", 0, 1), + done: true, + err: false, + }, + { + name: "update completes when all replicas are current", + generation: 1, + strategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType}, + status: apps.StatefulSetStatus{ + ObservedGeneration: 2, + Replicas: 3, + ReadyReplicas: 3, + CurrentReplicas: 3, + UpdatedReplicas: 3, + CurrentRevision: "foo", + UpdateRevision: "foo", + }, + + msg: fmt.Sprintf("statefulset rolling update complete %d pods at revision %s...\n", 3, "foo"), + done: true, + err: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := newStatefulSet(3) + s.Status = test.status + s.Spec.UpdateStrategy = test.strategy + s.Generation = test.generation + + unstructuredS := &unstructured.Unstructured{} + err := scheme.Scheme.Convert(s, unstructuredS, nil) + if err != nil { + t.Fatal(err) + } + + dsv := &StatefulSetStatusViewer{} + msg, done, err := dsv.Status(unstructuredS, 0) + if test.err && err == nil { + t.Fatalf("%s: expected error", test.name) + } + if !test.err && err != nil { + t.Fatalf("%s: %s", test.name, err) + } + if done && !test.done { + t.Errorf("%s: want done %v got %v", test.name, done, test.done) + } + if msg != test.msg { + t.Errorf("%s: want message %s got %s", test.name, test.msg, msg) + } + }) + } +} + +func TestDaemonSetStatusViewerStatusWithWrongUpdateStrategyType(t *testing.T) { + d := &apps.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "bar", + Name: "foo", + UID: "8764ae47-9092-11e4-8393-42010af018ff", + }, + Spec: apps.DaemonSetSpec{ + UpdateStrategy: apps.DaemonSetUpdateStrategy{ + Type: apps.OnDeleteDaemonSetStrategyType, + }, + }, + } + + unstructuredD := &unstructured.Unstructured{} + err := scheme.Scheme.Convert(d, unstructuredD, nil) + if err != nil { + t.Fatal(err) + } + + dsv := &DaemonSetStatusViewer{} + msg, done, err := dsv.Status(unstructuredD, 0) + errMsg := "rollout status is only available for RollingUpdate strategy type" + if err == nil || err.Error() != errMsg { + t.Errorf("Status for daemon sets with UpdateStrategy type different than RollingUpdate should return error. Instead got: msg: %s\ndone: %t\n err: %v", msg, done, err) + } +} + +func newStatefulSet(replicas int32) *apps.StatefulSet { + return &apps.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: metav1.NamespaceDefault, + Labels: map[string]string{"a": "b"}, + }, + Spec: apps.StatefulSetSpec{ + PodManagementPolicy: apps.OrderedReadyPodManagement, + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}}, + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"a": "b"}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "test", + Image: "test_image", + ImagePullPolicy: api.PullIfNotPresent, + }, + }, + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + }, + }, + Replicas: &replicas, + UpdateStrategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType}, + }, + Status: apps.StatefulSetStatus{}, + } +} diff --git a/pkg/polymorphichelpers/statusviewer.go b/pkg/polymorphichelpers/statusviewer.go new file mode 100644 index 000000000..0d6dd39f4 --- /dev/null +++ b/pkg/polymorphichelpers/statusviewer.go @@ -0,0 +1,26 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "k8s.io/apimachinery/pkg/api/meta" +) + +// statusViewer returns a StatusViewer for printing rollout status. +func statusViewer(mapping *meta.RESTMapping) (StatusViewer, error) { + return StatusViewerFor(mapping.GroupVersionKind.GroupKind()) +} diff --git a/pkg/polymorphichelpers/updatepodspec.go b/pkg/polymorphichelpers/updatepodspec.go new file mode 100644 index 000000000..404486382 --- /dev/null +++ b/pkg/polymorphichelpers/updatepodspec.go @@ -0,0 +1,91 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + batchv2alpha1 "k8s.io/api/batch/v2alpha1" + "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func updatePodSpecForObject(obj runtime.Object, fn func(*v1.PodSpec) error) (bool, error) { + switch t := obj.(type) { + case *v1.Pod: + return true, fn(&t.Spec) + // ReplicationController + case *v1.ReplicationController: + if t.Spec.Template == nil { + t.Spec.Template = &v1.PodTemplateSpec{} + } + return true, fn(&t.Spec.Template.Spec) + + // Deployment + case *extensionsv1beta1.Deployment: + return true, fn(&t.Spec.Template.Spec) + case *appsv1beta1.Deployment: + return true, fn(&t.Spec.Template.Spec) + case *appsv1beta2.Deployment: + return true, fn(&t.Spec.Template.Spec) + case *appsv1.Deployment: + return true, fn(&t.Spec.Template.Spec) + + // DaemonSet + case *extensionsv1beta1.DaemonSet: + return true, fn(&t.Spec.Template.Spec) + case *appsv1beta2.DaemonSet: + return true, fn(&t.Spec.Template.Spec) + case *appsv1.DaemonSet: + return true, fn(&t.Spec.Template.Spec) + + // ReplicaSet + case *extensionsv1beta1.ReplicaSet: + return true, fn(&t.Spec.Template.Spec) + case *appsv1beta2.ReplicaSet: + return true, fn(&t.Spec.Template.Spec) + case *appsv1.ReplicaSet: + return true, fn(&t.Spec.Template.Spec) + + // StatefulSet + case *appsv1beta1.StatefulSet: + return true, fn(&t.Spec.Template.Spec) + case *appsv1beta2.StatefulSet: + return true, fn(&t.Spec.Template.Spec) + case *appsv1.StatefulSet: + return true, fn(&t.Spec.Template.Spec) + + // Job + case *batchv1.Job: + return true, fn(&t.Spec.Template.Spec) + + // CronJob + case *batchv1beta1.CronJob: + return true, fn(&t.Spec.JobTemplate.Spec.Template.Spec) + case *batchv2alpha1.CronJob: + return true, fn(&t.Spec.JobTemplate.Spec.Template.Spec) + + default: + return false, fmt.Errorf("the object is not a pod or does not have a pod template: %T", t) + } +} diff --git a/pkg/polymorphichelpers/updatepodspec_test.go b/pkg/polymorphichelpers/updatepodspec_test.go new file mode 100644 index 000000000..abf98faed --- /dev/null +++ b/pkg/polymorphichelpers/updatepodspec_test.go @@ -0,0 +1,131 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "testing" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + batchv2alpha1 "k8s.io/api/batch/v2alpha1" + "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestUpdatePodSpecForObject(t *testing.T) { + tests := []struct { + object runtime.Object + expect bool + expectErr bool + }{ + { + object: &v1.Pod{}, + expect: true, + }, + { + object: &v1.ReplicationController{}, + expect: true, + }, + { + object: &extensionsv1beta1.Deployment{}, + expect: true, + }, + { + object: &appsv1beta1.Deployment{}, + expect: true, + }, + { + object: &appsv1beta2.Deployment{}, + expect: true, + }, + { + object: &appsv1.Deployment{}, + expect: true, + }, + { + object: &extensionsv1beta1.DaemonSet{}, + expect: true, + }, { + object: &appsv1beta2.DaemonSet{}, + expect: true, + }, + { + object: &appsv1.DaemonSet{}, + expect: true, + }, + { + object: &extensionsv1beta1.ReplicaSet{}, + expect: true, + }, + { + object: &appsv1beta2.ReplicaSet{}, + expect: true, + }, + { + object: &appsv1.ReplicaSet{}, + expect: true, + }, + { + object: &appsv1beta1.StatefulSet{}, + expect: true, + }, + { + object: &appsv1beta2.StatefulSet{}, + expect: true, + }, + { + object: &appsv1.StatefulSet{}, + expect: true, + }, + { + object: &batchv1.Job{}, + expect: true, + }, + { + object: &batchv1beta1.CronJob{}, + expect: true, + }, + { + object: &batchv2alpha1.CronJob{}, + expect: true, + }, + { + object: &v1.Node{}, + expect: false, + expectErr: true, + }, + } + + for _, test := range tests { + actual, err := updatePodSpecForObject(test.object, func(*v1.PodSpec) error { + return nil + }) + if test.expectErr && err == nil { + t.Error("unexpected non-error") + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + if actual != test.expect { + t.Errorf("expected %v, but got %v", test.expect, actual) + } + } +}