From aa4390750cb689c1663ac9c59052562499e3f3ea Mon Sep 17 00:00:00 2001 From: Maciej Szulik Date: Wed, 8 Mar 2017 11:11:29 +0100 Subject: [PATCH] Introduce new generator for apps/v1beta1 deployments --- hack/make-rules/test-cmd-util.sh | 46 ++++++-- pkg/kubectl/BUILD | 2 + pkg/kubectl/cmd/BUILD | 1 + pkg/kubectl/cmd/create.go | 2 +- pkg/kubectl/cmd/create_deployment.go | 29 ++++- pkg/kubectl/cmd/create_deployment_test.go | 39 +++++- pkg/kubectl/cmd/run.go | 47 +++++--- pkg/kubectl/cmd/run_test.go | 18 ++- pkg/kubectl/cmd/util/factory_client_access.go | 68 ++++++----- pkg/kubectl/deployment.go | 89 ++++++++++++++ pkg/kubectl/deployment_test.go | 111 ++++++++++++++++++ pkg/kubectl/run.go | 101 ++++++++++++++++ pkg/kubectl/run_test.go | 94 +++++++++++++++ 13 files changed, 578 insertions(+), 69 deletions(-) diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 59b86209d65..e57398c4e14 100644 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -1046,11 +1046,23 @@ run_kubectl_run_tests() { # Pre-Condition: no Deployment exists kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" '' # Command - kubectl run nginx "--image=$IMAGE_NGINX" --generator=deployment/v1beta1 "${kube_flags[@]}" + kubectl run nginx-extensions "--image=$IMAGE_NGINX" "${kube_flags[@]}" # Post-Condition: Deployment "nginx" is created - kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" 'nginx:' + kube::test::get_object_assert deployment.extensions "{{range.items}}{{$id_field}}:{{end}}" 'nginx-extensions:' + # and old generator was used, iow. old defaults are applied + output_message=$(kubectl get deployment.extensions/nginx-extensions -o jsonpath='{.spec.revisionHistoryLimit}') + kube::test::if_has_not_string "${output_message}" '2' # Clean up - kubectl delete deployment nginx "${kube_flags[@]}" + kubectl delete deployment nginx-extensions "${kube_flags[@]}" + # Command + kubectl run nginx-apps "--image=$IMAGE_NGINX" --generator=deployment/apps.v1beta1 "${kube_flags[@]}" + # Post-Condition: Deployment "nginx" is created + kube::test::get_object_assert deployment.apps "{{range.items}}{{$id_field}}:{{end}}" 'nginx-apps:' + # and new generator was used, iow. new defaults are applied + output_message=$(kubectl get deployment/nginx-apps -o jsonpath='{.spec.revisionHistoryLimit}') + kube::test::if_has_string "${output_message}" '2' + # Clean up + kubectl delete deployment nginx-apps "${kube_flags[@]}" } run_kubectl_get_tests() { @@ -2283,17 +2295,35 @@ run_rc_tests() { } run_deployment_tests() { - # Test kubectl create deployment - kubectl create deployment test-nginx --image=gcr.io/google-containers/nginx:test-cmd - # Post-Condition: Deployment has 2 replicas defined in its spec. - kube::test::get_object_assert 'deploy test-nginx' "{{$container_name_field}}" 'nginx' + # Test kubectl create deployment (using default - old generator) + kubectl create deployment test-nginx-extensions --image=gcr.io/google-containers/nginx:test-cmd + # Post-Condition: Deployment "nginx" is created. + kube::test::get_object_assert 'deploy test-nginx-extensions' "{{$container_name_field}}" 'nginx' + # and old generator was used, iow. old defaults are applied + output_message=$(kubectl get deployment.extensions/test-nginx-extensions -o jsonpath='{.spec.revisionHistoryLimit}') + kube::test::if_has_not_string "${output_message}" '2' # Ensure we can interact with deployments through extensions and apps endpoints output_message=$(kubectl get deployment.extensions -o=jsonpath='{.items[0].apiVersion}' 2>&1 "${kube_flags[@]}") kube::test::if_has_string "${output_message}" 'extensions/v1beta1' output_message=$(kubectl get deployment.apps -o=jsonpath='{.items[0].apiVersion}' 2>&1 "${kube_flags[@]}") kube::test::if_has_string "${output_message}" 'apps/v1beta1' # Clean up - kubectl delete deployment test-nginx "${kube_flags[@]}" + kubectl delete deployment test-nginx-extensions "${kube_flags[@]}" + + # Test kubectl create deployment + kubectl create deployment test-nginx-apps --image=gcr.io/google-containers/nginx:test-cmd --generator=deployment-basic/apps.v1beta1 + # Post-Condition: Deployment "nginx" is created. + kube::test::get_object_assert 'deploy test-nginx-apps' "{{$container_name_field}}" 'nginx' + # and new generator was used, iow. new defaults are applied + output_message=$(kubectl get deployment/test-nginx-apps -o jsonpath='{.spec.revisionHistoryLimit}') + kube::test::if_has_string "${output_message}" '2' + # Ensure we can interact with deployments through extensions and apps endpoints + output_message=$(kubectl get deployment.extensions -o=jsonpath='{.items[0].apiVersion}' 2>&1 "${kube_flags[@]}") + kube::test::if_has_string "${output_message}" 'extensions/v1beta1' + output_message=$(kubectl get deployment.apps -o=jsonpath='{.items[0].apiVersion}' 2>&1 "${kube_flags[@]}") + kube::test::if_has_string "${output_message}" 'apps/v1beta1' + # Clean up + kubectl delete deployment test-nginx-apps "${kube_flags[@]}" ### Test cascading deletion ## Test that rs is deleted when deployment is deleted. diff --git a/pkg/kubectl/BUILD b/pkg/kubectl/BUILD index 86b2b708736..9138c980c93 100644 --- a/pkg/kubectl/BUILD +++ b/pkg/kubectl/BUILD @@ -53,6 +53,7 @@ go_library( "//pkg/api/util:go_default_library", "//pkg/api/v1:go_default_library", "//pkg/apis/apps:go_default_library", + "//pkg/apis/apps/v1beta1:go_default_library", "//pkg/apis/autoscaling:go_default_library", "//pkg/apis/batch:go_default_library", "//pkg/apis/batch/v1:go_default_library", @@ -137,6 +138,7 @@ go_test( "//pkg/api/testapi:go_default_library", "//pkg/api/testing:go_default_library", "//pkg/api/v1:go_default_library", + "//pkg/apis/apps/v1beta1:go_default_library", "//pkg/apis/batch:go_default_library", "//pkg/apis/batch/v1:go_default_library", "//pkg/apis/batch/v2alpha1:go_default_library", diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index 87d5f084fa5..23e36b23799 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -70,6 +70,7 @@ go_library( "//pkg/api/annotations:go_default_library", "//pkg/api/v1:go_default_library", "//pkg/api/validation:go_default_library", + "//pkg/apis/apps/v1beta1:go_default_library", "//pkg/apis/batch/v1:go_default_library", "//pkg/apis/certificates:go_default_library", "//pkg/apis/extensions/v1beta1:go_default_library", diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 17d9089c536..e4da50b9468 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -94,7 +94,7 @@ func NewCmdCreate(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { cmd.AddCommand(NewCmdCreateConfigMap(f, out)) cmd.AddCommand(NewCmdCreateServiceAccount(f, out)) cmd.AddCommand(NewCmdCreateService(f, out, errOut)) - cmd.AddCommand(NewCmdCreateDeployment(f, out)) + cmd.AddCommand(NewCmdCreateDeployment(f, out, errOut)) cmd.AddCommand(NewCmdCreateClusterRole(f, out)) cmd.AddCommand(NewCmdCreateClusterRoleBinding(f, out)) cmd.AddCommand(NewCmdCreateRole(f, out)) diff --git a/pkg/kubectl/cmd/create_deployment.go b/pkg/kubectl/cmd/create_deployment.go index 179bdef38c2..fe3c434d998 100644 --- a/pkg/kubectl/cmd/create_deployment.go +++ b/pkg/kubectl/cmd/create_deployment.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" + appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" "k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" @@ -38,7 +39,7 @@ var ( ) // NewCmdCreateDeployment is a macro command to create a new deployment -func NewCmdCreateDeployment(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command { +func NewCmdCreateDeployment(f cmdutil.Factory, cmdOut, cmdErr io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "deployment NAME --image=image [--dry-run]", Aliases: []string{"deploy"}, @@ -46,7 +47,7 @@ func NewCmdCreateDeployment(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command Long: deploymentLong, Example: deploymentExample, Run: func(cmd *cobra.Command, args []string) { - err := CreateDeployment(f, cmdOut, cmd, args) + err := CreateDeployment(f, cmdOut, cmdErr, cmd, args) cmdutil.CheckErr(err) }, } @@ -60,13 +61,33 @@ func NewCmdCreateDeployment(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command } // CreateDeployment implements the behavior to run the create deployment command -func CreateDeployment(f cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command, args []string) error { +func CreateDeployment(f cmdutil.Factory, cmdOut, cmdErr io.Writer, cmd *cobra.Command, args []string) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { return err } + + clientset, err := f.ClientSet() + if err != nil { + return err + } + resourcesList, err := clientset.Discovery().ServerResources() + // ServerResources ignores errors for old servers do not expose discovery + if err != nil { + return fmt.Errorf("failed to discover supported resources: %v", err) + } + generatorName := cmdutil.GetFlagString(cmd, "generator") + // fallback to the old generator if server does not support apps/v1beta1 deployments + if generatorName == cmdutil.DeploymentBasicAppsV1Beta1GeneratorName && + !contains(resourcesList, appsv1beta1.SchemeGroupVersion.WithResource("deployments")) { + fmt.Fprintf(cmdErr, "WARNING: New deployments generator specified (%s), but apps/v1beta1.Deployments are not available, falling back to the old one (%s).\n", + cmdutil.DeploymentBasicAppsV1Beta1GeneratorName, cmdutil.DeploymentBasicV1Beta1GeneratorName) + generatorName = cmdutil.DeploymentBasicV1Beta1GeneratorName + } var generator kubectl.StructuredGenerator - switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { + switch generatorName { + case cmdutil.DeploymentBasicAppsV1Beta1GeneratorName: + generator = &kubectl.DeploymentBasicAppsGeneratorV1{Name: name, Images: cmdutil.GetFlagStringSlice(cmd, "image")} case cmdutil.DeploymentBasicV1Beta1GeneratorName: generator = &kubectl.DeploymentBasicGeneratorV1{Name: name, Images: cmdutil.GetFlagStringSlice(cmd, "image")} default: diff --git a/pkg/kubectl/cmd/create_deployment_test.go b/pkg/kubectl/cmd/create_deployment_test.go index 026d0f55716..067f7e650f1 100644 --- a/pkg/kubectl/cmd/create_deployment_test.go +++ b/pkg/kubectl/cmd/create_deployment_test.go @@ -18,20 +18,37 @@ package cmd import ( "bytes" + "io/ioutil" + "net/http" "testing" "github.com/stretchr/testify/assert" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/rest/fake" + "k8s.io/kubernetes/pkg/api" cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" ) func TestCreateDeployment(t *testing.T) { depName := "jonny-dep" - f, tf, _, _ := cmdtesting.NewAPIFactory() + f, tf, _, ns := cmdtesting.NewAPIFactory() + tf.Client = &fake.RESTClient{ + APIRegistry: api.Registry, + NegotiatedSerializer: ns, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(&bytes.Buffer{}), + }, nil + }), + } + tf.ClientConfig = &restclient.Config{} tf.Printer = &testPrinter{} tf.Namespace = "test" buf := bytes.NewBuffer([]byte{}) - cmd := NewCmdCreateDeployment(f, buf) + + cmd := NewCmdCreateDeployment(f, buf, buf) cmd.Flags().Set("dry-run", "true") cmd.Flags().Set("output", "name") cmd.Flags().Set("image", "hollywood/jonny.depp:v2") @@ -44,13 +61,25 @@ func TestCreateDeployment(t *testing.T) { func TestCreateDeploymentNoImage(t *testing.T) { depName := "jonny-dep" - f, tf, _, _ := cmdtesting.NewAPIFactory() + f, tf, _, ns := cmdtesting.NewAPIFactory() + tf.Client = &fake.RESTClient{ + APIRegistry: api.Registry, + NegotiatedSerializer: ns, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(&bytes.Buffer{}), + }, nil + }), + } + tf.ClientConfig = &restclient.Config{} tf.Printer = &testPrinter{} tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) - cmd := NewCmdCreateDeployment(f, buf) + cmd := NewCmdCreateDeployment(f, buf, buf) cmd.Flags().Set("dry-run", "true") cmd.Flags().Set("output", "name") - err := CreateDeployment(f, buf, cmd, []string{depName}) + err := CreateDeployment(f, buf, buf, cmd, []string{depName}) assert.Error(t, err, "at least one image must be specified") } diff --git a/pkg/kubectl/cmd/run.go b/pkg/kubectl/cmd/run.go index 01e4a01ceb5..33f5db611cb 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -33,8 +33,9 @@ import ( "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" "k8s.io/kubernetes/pkg/api" + appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" batchv1 "k8s.io/kubernetes/pkg/apis/batch/v1" - "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" + extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" conditions "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/kubectl" @@ -196,38 +197,50 @@ func Run(f cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cobr return err } + clientset, err := f.ClientSet() + if err != nil { + return err + } + resourcesList, err := clientset.Discovery().ServerResources() + // ServerResources ignores errors for old servers do not expose discovery + if err != nil { + return fmt.Errorf("failed to discover supported resources: %v", err) + } + generatorName := cmdutil.GetFlagString(cmd, "generator") schedule := cmdutil.GetFlagString(cmd, "schedule") if len(schedule) != 0 && len(generatorName) == 0 { - generatorName = "cronjob/v2alpha1" + generatorName = cmdutil.CronJobV2Alpha1GeneratorName } if len(generatorName) == 0 { - clientset, err := f.ClientSet() - if err != nil { - return err - } - resourcesList, err := clientset.Discovery().ServerResources() - // ServerResources ignores errors for old servers do not expose discovery - if err != nil { - return fmt.Errorf("failed to discover supported resources: %v", err) - } switch restartPolicy { case api.RestartPolicyAlways: - if contains(resourcesList, v1beta1.SchemeGroupVersion.WithResource("deployments")) { - generatorName = "deployment/v1beta1" + // TODO: we need to deprecate this along with extensions/v1beta1.Deployments + // in favor of the new generator for apps/v1beta1.Deployments + if contains(resourcesList, extensionsv1beta1.SchemeGroupVersion.WithResource("deployments")) { + generatorName = cmdutil.DeploymentV1Beta1GeneratorName } else { - generatorName = "run/v1" + generatorName = cmdutil.RunV1GeneratorName } case api.RestartPolicyOnFailure: if contains(resourcesList, batchv1.SchemeGroupVersion.WithResource("jobs")) { - generatorName = "job/v1" + generatorName = cmdutil.JobV1GeneratorName } else { - generatorName = "run-pod/v1" + generatorName = cmdutil.RunPodV1GeneratorName } case api.RestartPolicyNever: - generatorName = "run-pod/v1" + generatorName = cmdutil.RunPodV1GeneratorName } } + + // TODO: this should be removed alongside with extensions/v1beta1 depployments generator + if generatorName == cmdutil.DeploymentAppsV1Beta1GeneratorName && + !contains(resourcesList, appsv1beta1.SchemeGroupVersion.WithResource("deployments")) { + fmt.Fprintf(cmdErr, "WARNING: New deployments generator specified (%s), but apps/v1beta1.Deployments are not available, falling back to the old one (%s).\n", + cmdutil.DeploymentAppsV1Beta1GeneratorName, cmdutil.DeploymentV1Beta1GeneratorName) + generatorName = cmdutil.DeploymentV1Beta1GeneratorName + } + generators := f.Generators("run") generator, found := generators[generatorName] if !found { diff --git a/pkg/kubectl/cmd/run_test.go b/pkg/kubectl/cmd/run_test.go index 4af6c35454c..eff0e8864c5 100644 --- a/pkg/kubectl/cmd/run_test.go +++ b/pkg/kubectl/cmd/run_test.go @@ -35,6 +35,7 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/client-go/rest/fake" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/v1" cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" ) @@ -113,7 +114,13 @@ func TestGetEnv(t *testing.T) { } func TestRunArgsFollowDashRules(t *testing.T) { - _, _, rc := testData() + one := int32(1) + rc := &v1.ReplicationController{ + ObjectMeta: metav1.ObjectMeta{Name: "rc1", Namespace: "test", ResourceVersion: "18"}, + Spec: v1.ReplicationControllerSpec{ + Replicas: &one, + }, + } tests := []struct { args []string @@ -158,7 +165,14 @@ func TestRunArgsFollowDashRules(t *testing.T) { APIRegistry: api.Registry, NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, &rc.Items[0])}, nil + if req.URL.Path == "/namespaces/test/replicationcontrollers" { + return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, rc)}, nil + } else { + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(&bytes.Buffer{}), + }, nil + } }), } tf.Namespace = "test" diff --git a/pkg/kubectl/cmd/util/factory_client_access.go b/pkg/kubectl/cmd/util/factory_client_access.go index 6338868927c..5d435ab270e 100644 --- a/pkg/kubectl/cmd/util/factory_client_access.go +++ b/pkg/kubectl/cmd/util/factory_client_access.go @@ -456,31 +456,33 @@ func (f *ring0Factory) DefaultNamespace() (string, bool, error) { } const ( - RunV1GeneratorName = "run/v1" - RunPodV1GeneratorName = "run-pod/v1" - ServiceV1GeneratorName = "service/v1" - ServiceV2GeneratorName = "service/v2" - ServiceNodePortGeneratorV1Name = "service-nodeport/v1" - ServiceClusterIPGeneratorV1Name = "service-clusterip/v1" - ServiceLoadBalancerGeneratorV1Name = "service-loadbalancer/v1" - ServiceExternalNameGeneratorV1Name = "service-externalname/v1" - ServiceAccountV1GeneratorName = "serviceaccount/v1" - HorizontalPodAutoscalerV1GeneratorName = "horizontalpodautoscaler/v1" - DeploymentV1Beta1GeneratorName = "deployment/v1beta1" - DeploymentBasicV1Beta1GeneratorName = "deployment-basic/v1beta1" - JobV1GeneratorName = "job/v1" - CronJobV2Alpha1GeneratorName = "cronjob/v2alpha1" - ScheduledJobV2Alpha1GeneratorName = "scheduledjob/v2alpha1" - NamespaceV1GeneratorName = "namespace/v1" - ResourceQuotaV1GeneratorName = "resourcequotas/v1" - SecretV1GeneratorName = "secret/v1" - SecretForDockerRegistryV1GeneratorName = "secret-for-docker-registry/v1" - SecretForTLSV1GeneratorName = "secret-for-tls/v1" - ConfigMapV1GeneratorName = "configmap/v1" - ClusterRoleBindingV1GeneratorName = "clusterrolebinding.rbac.authorization.k8s.io/v1alpha1" - RoleBindingV1GeneratorName = "rolebinding.rbac.authorization.k8s.io/v1alpha1" - ClusterV1Beta1GeneratorName = "cluster/v1beta1" - PodDisruptionBudgetV1GeneratorName = "poddisruptionbudget/v1beta1" + RunV1GeneratorName = "run/v1" + RunPodV1GeneratorName = "run-pod/v1" + ServiceV1GeneratorName = "service/v1" + ServiceV2GeneratorName = "service/v2" + ServiceNodePortGeneratorV1Name = "service-nodeport/v1" + ServiceClusterIPGeneratorV1Name = "service-clusterip/v1" + ServiceLoadBalancerGeneratorV1Name = "service-loadbalancer/v1" + ServiceExternalNameGeneratorV1Name = "service-externalname/v1" + ServiceAccountV1GeneratorName = "serviceaccount/v1" + HorizontalPodAutoscalerV1GeneratorName = "horizontalpodautoscaler/v1" + DeploymentV1Beta1GeneratorName = "deployment/v1beta1" + DeploymentAppsV1Beta1GeneratorName = "deployment/apps.v1beta1" + DeploymentBasicV1Beta1GeneratorName = "deployment-basic/v1beta1" + DeploymentBasicAppsV1Beta1GeneratorName = "deployment-basic/apps.v1beta1" + JobV1GeneratorName = "job/v1" + CronJobV2Alpha1GeneratorName = "cronjob/v2alpha1" + ScheduledJobV2Alpha1GeneratorName = "scheduledjob/v2alpha1" + NamespaceV1GeneratorName = "namespace/v1" + ResourceQuotaV1GeneratorName = "resourcequotas/v1" + SecretV1GeneratorName = "secret/v1" + SecretForDockerRegistryV1GeneratorName = "secret-for-docker-registry/v1" + SecretForTLSV1GeneratorName = "secret-for-tls/v1" + ConfigMapV1GeneratorName = "configmap/v1" + ClusterRoleBindingV1GeneratorName = "clusterrolebinding.rbac.authorization.k8s.io/v1alpha1" + RoleBindingV1GeneratorName = "rolebinding.rbac.authorization.k8s.io/v1alpha1" + ClusterV1Beta1GeneratorName = "cluster/v1beta1" + PodDisruptionBudgetV1GeneratorName = "poddisruptionbudget/v1beta1" ) // DefaultGenerators returns the set of default generators for use in Factory instances @@ -506,16 +508,18 @@ func DefaultGenerators(cmdName string) map[string]kubectl.Generator { } case "deployment": generator = map[string]kubectl.Generator{ - DeploymentBasicV1Beta1GeneratorName: kubectl.DeploymentBasicGeneratorV1{}, + DeploymentBasicV1Beta1GeneratorName: kubectl.DeploymentBasicGeneratorV1{}, + DeploymentBasicAppsV1Beta1GeneratorName: kubectl.DeploymentBasicAppsGeneratorV1{}, } case "run": generator = map[string]kubectl.Generator{ - RunV1GeneratorName: kubectl.BasicReplicationController{}, - RunPodV1GeneratorName: kubectl.BasicPod{}, - DeploymentV1Beta1GeneratorName: kubectl.DeploymentV1Beta1{}, - JobV1GeneratorName: kubectl.JobV1{}, - ScheduledJobV2Alpha1GeneratorName: kubectl.CronJobV2Alpha1{}, - CronJobV2Alpha1GeneratorName: kubectl.CronJobV2Alpha1{}, + RunV1GeneratorName: kubectl.BasicReplicationController{}, + RunPodV1GeneratorName: kubectl.BasicPod{}, + DeploymentV1Beta1GeneratorName: kubectl.DeploymentV1Beta1{}, + DeploymentAppsV1Beta1GeneratorName: kubectl.DeploymentAppsV1Beta1{}, + JobV1GeneratorName: kubectl.JobV1{}, + ScheduledJobV2Alpha1GeneratorName: kubectl.CronJobV2Alpha1{}, + CronJobV2Alpha1GeneratorName: kubectl.CronJobV2Alpha1{}, } case "autoscale": generator = map[string]kubectl.Generator{ diff --git a/pkg/kubectl/deployment.go b/pkg/kubectl/deployment.go index 56a1358e16d..9747155ab8e 100644 --- a/pkg/kubectl/deployment.go +++ b/pkg/kubectl/deployment.go @@ -23,6 +23,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/api/v1" + appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" ) @@ -113,3 +114,91 @@ func (s *DeploymentBasicGeneratorV1) validate() error { } return nil } + +// DeploymentBasicAppsGeneratorV1 supports stable generation of a deployment under apps/v1beta1 endpoint +type DeploymentBasicAppsGeneratorV1 struct { + Name string + Images []string +} + +// Ensure it supports the generator pattern that uses parameters specified during construction +var _ StructuredGenerator = &DeploymentBasicAppsGeneratorV1{} + +func (DeploymentBasicAppsGeneratorV1) ParamNames() []GeneratorParam { + return []GeneratorParam{ + {"name", true}, + {"image", true}, + } +} + +func (s DeploymentBasicAppsGeneratorV1) Generate(params map[string]interface{}) (runtime.Object, error) { + err := ValidateParams(s.ParamNames(), params) + if err != nil { + return nil, err + } + name, isString := params["name"].(string) + if !isString { + return nil, fmt.Errorf("expected string, saw %v for 'name'", name) + } + imageStrings, isArray := params["image"].([]string) + if !isArray { + return nil, fmt.Errorf("expected []string, found :%v", imageStrings) + } + delegate := &DeploymentBasicAppsGeneratorV1{Name: name, Images: imageStrings} + return delegate.StructuredGenerate() +} + +// StructuredGenerate outputs a deployment object using the configured fields +func (s *DeploymentBasicAppsGeneratorV1) StructuredGenerate() (runtime.Object, error) { + if err := s.validate(); err != nil { + return nil, err + } + + podSpec := v1.PodSpec{Containers: []v1.Container{}} + for _, imageString := range s.Images { + // Retain just the image name + imageSplit := strings.Split(imageString, "/") + name := imageSplit[len(imageSplit)-1] + // Remove any tag or hash + if strings.Contains(name, ":") { + name = strings.Split(name, ":")[0] + } else if strings.Contains(name, "@") { + name = strings.Split(name, "@")[0] + } + podSpec.Containers = append(podSpec.Containers, v1.Container{Name: name, Image: imageString}) + } + + // setup default label and selector + labels := map[string]string{} + labels["app"] = s.Name + one := int32(1) + selector := metav1.LabelSelector{MatchLabels: labels} + deployment := appsv1beta1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.Name, + Labels: labels, + }, + Spec: appsv1beta1.DeploymentSpec{ + Replicas: &one, + Selector: &selector, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: podSpec, + }, + }, + } + return &deployment, nil +} + +// validate validates required fields are set to support structured generation +func (s *DeploymentBasicAppsGeneratorV1) validate() error { + if len(s.Name) == 0 { + return fmt.Errorf("name must be specified") + } + if len(s.Images) == 0 { + return fmt.Errorf("at least one image must be specified") + } + return nil +} diff --git a/pkg/kubectl/deployment_test.go b/pkg/kubectl/deployment_test.go index bea83f098df..f6dc0a2f46e 100644 --- a/pkg/kubectl/deployment_test.go +++ b/pkg/kubectl/deployment_test.go @@ -22,6 +22,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/kubernetes/pkg/api/v1" + appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" ) @@ -134,3 +135,113 @@ func TestDeploymentGenerate(t *testing.T) { } } } + +func TestAppsDeploymentGenerate(t *testing.T) { + one := int32(1) + tests := []struct { + params map[string]interface{} + expected *appsv1beta1.Deployment + expectErr bool + }{ + { + params: map[string]interface{}{ + "name": "foo", + "image": []string{"abc/app:v4"}, + }, + expected: &appsv1beta1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"app": "foo"}, + }, + Spec: appsv1beta1.DeploymentSpec{ + Replicas: &one, + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "foo"}, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{{Name: "app", Image: "abc/app:v4"}}, + }, + }, + }, + }, + expectErr: false, + }, + { + params: map[string]interface{}{ + "name": "foo", + "image": []string{"abc/app:v4", "zyx/ape"}, + }, + expected: &appsv1beta1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"app": "foo"}, + }, + Spec: appsv1beta1.DeploymentSpec{ + Replicas: &one, + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "foo"}, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{{Name: "app", Image: "abc/app:v4"}, + {Name: "ape", Image: "zyx/ape"}}, + }, + }, + }, + }, + expectErr: false, + }, + { + params: map[string]interface{}{}, + expectErr: true, + }, + { + params: map[string]interface{}{ + "name": 1, + }, + expectErr: true, + }, + { + params: map[string]interface{}{ + "name": nil, + }, + expectErr: true, + }, + { + params: map[string]interface{}{ + "name": "foo", + "image": []string{}, + }, + expectErr: true, + }, + { + params: map[string]interface{}{ + "NAME": "some_value", + }, + expectErr: true, + }, + } + generator := DeploymentBasicAppsGeneratorV1{} + for index, test := range tests { + t.Logf("running scenario %d", index) + obj, err := generator.Generate(test.params) + switch { + case test.expectErr && err != nil: + continue // loop, since there's no output to check + case test.expectErr && err == nil: + t.Errorf("expected error and didn't get one") + continue // loop, no expected output object + case !test.expectErr && err != nil: + t.Errorf("unexpected error %v", err) + continue // loop, no output object + case !test.expectErr && err == nil: + // do nothing and drop through + } + if !reflect.DeepEqual(obj.(*appsv1beta1.Deployment), test.expected) { + t.Errorf("expected:\n%#v\nsaw:\n%#v", test.expected, obj.(*appsv1beta1.Deployment)) + } + } +} diff --git a/pkg/kubectl/run.go b/pkg/kubectl/run.go index d5d83df2b7c..8a25e5e8b9a 100644 --- a/pkg/kubectl/run.go +++ b/pkg/kubectl/run.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/v1" + appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" batchv1 "k8s.io/kubernetes/pkg/apis/batch/v1" batchv2alpha1 "k8s.io/kubernetes/pkg/apis/batch/v2alpha1" extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" @@ -121,6 +122,94 @@ func (DeploymentV1Beta1) Generate(genericParams map[string]interface{}) (runtime return &deployment, nil } +type DeploymentAppsV1Beta1 struct{} + +func (DeploymentAppsV1Beta1) ParamNames() []GeneratorParam { + return []GeneratorParam{ + {"labels", false}, + {"default-name", false}, + {"name", true}, + {"replicas", true}, + {"image", true}, + {"image-pull-policy", false}, + {"port", false}, + {"hostport", false}, + {"stdin", false}, + {"tty", false}, + {"command", false}, + {"args", false}, + {"env", false}, + {"requests", false}, + {"limits", false}, + } +} + +func (DeploymentAppsV1Beta1) Generate(genericParams map[string]interface{}) (runtime.Object, error) { + args, err := getArgs(genericParams) + if err != nil { + return nil, err + } + + envs, err := getEnvs(genericParams) + if err != nil { + return nil, err + } + + params, err := getParams(genericParams) + if err != nil { + return nil, err + } + + name, err := getName(params) + if err != nil { + return nil, err + } + + labels, err := getLabels(params, true, name) + if err != nil { + return nil, err + } + + count, err := strconv.Atoi(params["replicas"]) + if err != nil { + return nil, err + } + + podSpec, err := makePodSpec(params, name) + if err != nil { + return nil, err + } + + imagePullPolicy := v1.PullPolicy(params["image-pull-policy"]) + if err = updatePodContainers(params, args, envs, imagePullPolicy, podSpec); err != nil { + return nil, err + } + + if err := updatePodPorts(params, podSpec); err != nil { + return nil, err + } + + count32 := int32(count) + deployment := appsv1beta1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: labels, + }, + Spec: appsv1beta1.DeploymentSpec{ + Replicas: &count32, + Selector: &metav1.LabelSelector{MatchLabels: labels}, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: *podSpec, + }, + }, + } + return &deployment, nil +} + +// getLabels returns map of labels. func getLabels(params map[string]string, defaultRunLabel bool, name string) (map[string]string, error) { labelString, found := params["labels"] var labels map[string]string @@ -138,6 +227,7 @@ func getLabels(params map[string]string, defaultRunLabel bool, name string) (map return labels, nil } +// getName returns the name of newly created resource. func getName(params map[string]string) (string, error) { name, found := params["name"] if !found || len(name) == 0 { @@ -149,6 +239,7 @@ func getName(params map[string]string) (string, error) { return name, nil } +// getParams returns map of generic parameters. func getParams(genericParams map[string]interface{}) (map[string]string, error) { params := map[string]string{} for key, value := range genericParams { @@ -161,6 +252,7 @@ func getParams(genericParams map[string]interface{}) (map[string]string, error) return params, nil } +// getArgs returns arguments for the container command. func getArgs(genericParams map[string]interface{}) ([]string, error) { args := []string{} val, found := genericParams["args"] @@ -175,6 +267,7 @@ func getArgs(genericParams map[string]interface{}) ([]string, error) { return args, nil } +// getEnvs returns environment variables. func getEnvs(genericParams map[string]interface{}) ([]v1.EnvVar, error) { var envs []v1.EnvVar envStrings, found := genericParams["env"] @@ -409,6 +502,7 @@ func (BasicReplicationController) ParamNames() []GeneratorParam { } // populateResourceList takes strings of form =,= +// and returns ResourceList. func populateResourceList(spec string) (api.ResourceList, error) { // empty input gets a nil response to preserve generator test expected behaviors if spec == "" { @@ -433,6 +527,7 @@ func populateResourceList(spec string) (api.ResourceList, error) { } // populateResourceListV1 takes strings of form =,= +// and returns ResourceList. func populateResourceListV1(spec string) (v1.ResourceList, error) { // empty input gets a nil response to preserve generator test expected behaviors if spec == "" { @@ -457,6 +552,7 @@ func populateResourceListV1(spec string) (v1.ResourceList, error) { } // HandleResourceRequirements parses the limits and requests parameters if specified +// and returns ResourceRequirements. func HandleResourceRequirements(params map[string]string) (api.ResourceRequirements, error) { result := api.ResourceRequirements{} limits, err := populateResourceList(params["limits"]) @@ -473,6 +569,7 @@ func HandleResourceRequirements(params map[string]string) (api.ResourceRequireme } // HandleResourceRequirementsV1 parses the limits and requests parameters if specified +// and returns ResourceRequirements. func HandleResourceRequirementsV1(params map[string]string) (v1.ResourceRequirements, error) { result := v1.ResourceRequirements{} limits, err := populateResourceListV1(params["limits"]) @@ -488,6 +585,7 @@ func HandleResourceRequirementsV1(params map[string]string) (v1.ResourceRequirem return result, nil } +// makePodSpec returns PodSpec filled with passed parameters. func makePodSpec(params map[string]string, name string) (*v1.PodSpec, error) { stdin, err := GetBool(params, "stdin", false) if err != nil { @@ -583,6 +681,7 @@ func (BasicReplicationController) Generate(genericParams map[string]interface{}) return &controller, nil } +// updatePodContainers updates PodSpec.Containers with passed parameters. func updatePodContainers(params map[string]string, args []string, envs []v1.EnvVar, imagePullPolicy v1.PullPolicy, podSpec *v1.PodSpec) error { if len(args) > 0 { command, err := GetBool(params, "command", false) @@ -607,6 +706,7 @@ func updatePodContainers(params map[string]string, args []string, envs []v1.EnvV return nil } +// updatePodContainers updates PodSpec.Containers.Ports with passed parameters. func updatePodPorts(params map[string]string, podSpec *v1.PodSpec) (err error) { port := -1 hostPort := -1 @@ -747,6 +847,7 @@ func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object, return &pod, nil } +// parseEnvs converts string into EnvVar objects. func parseEnvs(envArray []string) ([]v1.EnvVar, error) { envs := make([]v1.EnvVar, 0, len(envArray)) for _, env := range envArray { diff --git a/pkg/kubectl/run_test.go b/pkg/kubectl/run_test.go index b42ec40fb0d..b3301758fd9 100644 --- a/pkg/kubectl/run_test.go +++ b/pkg/kubectl/run_test.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/kubernetes/pkg/api/v1" + appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" batchv1 "k8s.io/kubernetes/pkg/apis/batch/v1" batchv2alpha1 "k8s.io/kubernetes/pkg/apis/batch/v2alpha1" extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" @@ -733,6 +734,99 @@ func TestGenerateDeployment(t *testing.T) { } } +func TestGenerateAppsDeployment(t *testing.T) { + three := int32(3) + tests := []struct { + params map[string]interface{} + expected *appsv1beta1.Deployment + expectErr bool + }{ + { + params: map[string]interface{}{ + "labels": "foo=bar,baz=blah", + "name": "foo", + "replicas": "3", + "image": "someimage", + "image-pull-policy": "Always", + "port": "80", + "hostport": "80", + "stdin": "true", + "command": "true", + "args": []string{"bar", "baz", "blah"}, + "env": []string{"a=b", "c=d"}, + "requests": "cpu=100m,memory=100Mi", + "limits": "cpu=400m,memory=200Mi", + }, + expected: &appsv1beta1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"foo": "bar", "baz": "blah"}, + }, + Spec: appsv1beta1.DeploymentSpec{ + Replicas: &three, + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar", "baz": "blah"}}, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"foo": "bar", "baz": "blah"}, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "foo", + Image: "someimage", + ImagePullPolicy: v1.PullAlways, + Stdin: true, + Ports: []v1.ContainerPort{ + { + ContainerPort: 80, + HostPort: 80, + }, + }, + Command: []string{"bar", "baz", "blah"}, + Env: []v1.EnvVar{ + { + Name: "a", + Value: "b", + }, + { + Name: "c", + Value: "d", + }, + }, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("100Mi"), + }, + Limits: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("400m"), + v1.ResourceMemory: resource.MustParse("200Mi"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + generator := DeploymentAppsV1Beta1{} + for _, test := range tests { + obj, err := generator.Generate(test.params) + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v", err) + } + if test.expectErr && err != nil { + continue + } + if !reflect.DeepEqual(obj.(*appsv1beta1.Deployment), test.expected) { + t.Errorf("\nexpected:\n%#v\nsaw:\n%#v", test.expected, obj.(*appsv1beta1.Deployment)) + } + } +} + func TestGenerateJob(t *testing.T) { tests := []struct { params map[string]interface{}