Move pkg/kubectl/polymorphichelpers staging
Kubernetes-commit: 9f3384f02f472b0095fe3675139c08e7aaa7e711
This commit is contained in:
parent
98d4a9f0a1
commit
f266a308e1
23
go.mod
23
go.mod
|
|
@ -25,13 +25,13 @@ require (
|
||||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f
|
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
gopkg.in/yaml.v2 v2.2.2
|
||||||
gotest.tools v2.2.0+incompatible // indirect
|
gotest.tools v2.2.0+incompatible // indirect
|
||||||
k8s.io/api v0.0.0-20190726022912-69e1bce1dad5
|
k8s.io/api v0.0.0
|
||||||
k8s.io/apimachinery v0.0.0-20190727130956-f97a4e5b4abc
|
k8s.io/apimachinery v0.0.0
|
||||||
k8s.io/cli-runtime v0.0.0-20190726024606-74a61cd71909
|
k8s.io/cli-runtime v0.0.0
|
||||||
k8s.io/client-go v0.0.0-20190730143201-693ed41095c3
|
k8s.io/client-go v0.0.0
|
||||||
k8s.io/klog v0.3.1
|
k8s.io/klog v0.3.1
|
||||||
k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058
|
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
|
k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a
|
||||||
sigs.k8s.io/yaml v1.1.0
|
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/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/text => golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db
|
||||||
golang.org/x/tools => golang.org/x/tools v0.0.0-20190313210603-aa82965741a9
|
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/api => ../api
|
||||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190727130956-f97a4e5b4abc
|
k8s.io/apimachinery => ../apimachinery
|
||||||
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20190726024606-74a61cd71909
|
k8s.io/cli-runtime => ../cli-runtime
|
||||||
k8s.io/client-go => k8s.io/client-go v0.0.0-20190730143201-693ed41095c3
|
k8s.io/client-go => ../client-go
|
||||||
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20190726022633-14ba7d03f06f
|
k8s.io/code-generator => ../code-generator
|
||||||
k8s.io/metrics => k8s.io/metrics v0.0.0-20190726024513-9140f5fe6ab8
|
k8s.io/kubectl => ../kubectl
|
||||||
|
k8s.io/metrics => ../metrics
|
||||||
)
|
)
|
||||||
|
|
|
||||||
8
go.sum
8
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/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 h1:Kt4R9nQn1c+x/o63vCZuxo3WjBc8EnSfnguI4ELkdoo=
|
||||||
github.com/gogo/protobuf v0.0.0-20190410021324-65acae22fc9/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
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/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 v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
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/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 h1:6TSoaYExHper8PYsJu23GWVNOyYRCSnIFyxKgLSZ54w=
|
||||||
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
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/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/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
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=
|
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 h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
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/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.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||||
k8s.io/klog v0.3.0/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/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 h1:di3XCwddOR9cWBNpfgXaskhh6cgJuwcK54rvtwUaC10=
|
||||||
k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4=
|
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 h1:2jUDc9gJja832Ftp+QbDV0tVhQHMISFn01els+2ZAcw=
|
||||||
k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||||
modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
|
modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 = "<none>"
|
||||||
|
}
|
||||||
|
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 = "<none>"
|
||||||
|
}
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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, ",")
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue