From bd0b7528b5b34174bf9864f9e9a70a90ce734352 Mon Sep 17 00:00:00 2001 From: Angus Salkeld Date: Mon, 8 Aug 2016 13:24:30 +1000 Subject: [PATCH] Add "create deployment" sub-command --- .generated_docs | 2 + docs/man/man1/kubectl-create-deployment.1 | 3 + .../kubectl/kubectl_create_deployment.md | 36 +++++ pkg/kubectl/cmd/create.go | 1 + pkg/kubectl/cmd/create_deployment.go | 80 +++++++++++ pkg/kubectl/cmd/create_deployment_test.go | 54 +++++++ pkg/kubectl/cmd/util/factory.go | 4 + pkg/kubectl/deployment.go | 107 ++++++++++++++ pkg/kubectl/deployment_test.go | 134 ++++++++++++++++++ 9 files changed, 421 insertions(+) create mode 100644 docs/man/man1/kubectl-create-deployment.1 create mode 100644 docs/user-guide/kubectl/kubectl_create_deployment.md create mode 100644 pkg/kubectl/cmd/create_deployment.go create mode 100644 pkg/kubectl/cmd/create_deployment_test.go create mode 100644 pkg/kubectl/deployment.go create mode 100644 pkg/kubectl/deployment_test.go diff --git a/.generated_docs b/.generated_docs index 1cdfc537781..de64706d093 100644 --- a/.generated_docs +++ b/.generated_docs @@ -27,6 +27,7 @@ docs/man/man1/kubectl-config.1 docs/man/man1/kubectl-convert.1 docs/man/man1/kubectl-cordon.1 docs/man/man1/kubectl-create-configmap.1 +docs/man/man1/kubectl-create-deployment.1 docs/man/man1/kubectl-create-namespace.1 docs/man/man1/kubectl-create-quota.1 docs/man/man1/kubectl-create-secret-docker-registry.1 @@ -93,6 +94,7 @@ docs/user-guide/kubectl/kubectl_convert.md docs/user-guide/kubectl/kubectl_cordon.md docs/user-guide/kubectl/kubectl_create.md docs/user-guide/kubectl/kubectl_create_configmap.md +docs/user-guide/kubectl/kubectl_create_deployment.md docs/user-guide/kubectl/kubectl_create_namespace.md docs/user-guide/kubectl/kubectl_create_quota.md docs/user-guide/kubectl/kubectl_create_secret.md diff --git a/docs/man/man1/kubectl-create-deployment.1 b/docs/man/man1/kubectl-create-deployment.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubectl-create-deployment.1 @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/docs/user-guide/kubectl/kubectl_create_deployment.md b/docs/user-guide/kubectl/kubectl_create_deployment.md new file mode 100644 index 00000000000..d2306069c4f --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_create_deployment.md @@ -0,0 +1,36 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +If you are using a released version of Kubernetes, you should +refer to the docs that go with that version. + +Documentation for other releases can be found at +[releases.k8s.io](http://releases.k8s.io). + +-- + + + + + +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_create_deployment.md?pixel)]() + diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index f23614a852d..0fab1339329 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -85,6 +85,7 @@ func NewCmdCreate(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.AddCommand(NewCmdCreateConfigMap(f, out)) cmd.AddCommand(NewCmdCreateServiceAccount(f, out)) cmd.AddCommand(NewCmdCreateService(f, out)) + cmd.AddCommand(NewCmdCreateDeployment(f, out)) return cmd } diff --git a/pkg/kubectl/cmd/create_deployment.go b/pkg/kubectl/cmd/create_deployment.go new file mode 100644 index 00000000000..007ebbe5cfd --- /dev/null +++ b/pkg/kubectl/cmd/create_deployment.go @@ -0,0 +1,80 @@ +/* +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 cmd + +import ( + "fmt" + "io" + + "github.com/renstrom/dedent" + "github.com/spf13/cobra" + + "k8s.io/kubernetes/pkg/kubectl" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" +) + +var ( + deploymentLong = dedent.Dedent(` + Create a deployment with the specified name.`) + + deploymentExample = dedent.Dedent(` + # Create a new deployment named my-dep that runs the busybox image. + kubectl create deployment my-dep --image=busybox`) +) + +// NewCmdCreateDeployment is a macro command to create a new deployment +func NewCmdCreateDeployment(f *cmdutil.Factory, cmdOut io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "deployment NAME --image=image [--dry-run]", + Aliases: []string{"dep"}, + Short: "Create a deployment with the specified name.", + Long: deploymentLong, + Example: deploymentExample, + Run: func(cmd *cobra.Command, args []string) { + err := CreateDeployment(f, cmdOut, cmd, args) + cmdutil.CheckErr(err) + }, + } + cmdutil.AddApplyAnnotationFlags(cmd) + cmdutil.AddValidateFlags(cmd) + cmdutil.AddPrinterFlags(cmd) + cmdutil.AddGeneratorFlags(cmd, cmdutil.DeploymentBasicV1Beta1GeneratorName) + cmd.Flags().StringSlice("image", []string{}, "Image name to run.") + cmd.MarkFlagRequired("image") + return cmd +} + +// CreateDeployment implements the behavior to run the create deployment command +func CreateDeployment(f *cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command, args []string) error { + name, err := NameFromCommandArgs(cmd, args) + if err != nil { + return err + } + var generator kubectl.StructuredGenerator + switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { + case cmdutil.DeploymentBasicV1Beta1GeneratorName: + generator = &kubectl.DeploymentBasicGeneratorV1{Name: name, Images: cmdutil.GetFlagStringSlice(cmd, "image")} + default: + return cmdutil.UsageError(cmd, fmt.Sprintf("Generator: %s not supported.", generatorName)) + } + return RunCreateSubcommand(f, cmd, cmdOut, &CreateSubcommandOptions{ + Name: name, + StructuredGenerator: generator, + DryRun: cmdutil.GetDryRunFlag(cmd), + OutputFormat: cmdutil.GetFlagString(cmd, "output"), + }) +} diff --git a/pkg/kubectl/cmd/create_deployment_test.go b/pkg/kubectl/cmd/create_deployment_test.go new file mode 100644 index 00000000000..36d4b1a58cc --- /dev/null +++ b/pkg/kubectl/cmd/create_deployment_test.go @@ -0,0 +1,54 @@ +/* +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 cmd + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreateDeployment(t *testing.T) { + depName := "jonny-dep" + f, tf, _, _ := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + cmd := NewCmdCreateDeployment(f, buf) + cmd.Flags().Set("dry-run", "true") + cmd.Flags().Set("output", "name") + cmd.Flags().Set("image", "hollywood/jonny.depp:v2") + cmd.Run(cmd, []string{depName}) + expectedOutput := "deployment/" + depName + "\n" + if buf.String() != expectedOutput { + t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) + } +} + +func TestCreateDeploymentNoImage(t *testing.T) { + depName := "jonny-dep" + f, tf, _, _ := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + cmd := NewCmdCreateDeployment(f, buf) + cmd.Flags().Set("dry-run", "true") + cmd.Flags().Set("output", "name") + err := CreateDeployment(f, buf, cmd, []string{depName}) + assert.Error(t, err, "at least one image must be specified") +} diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index eadfe07533b..08000d846ca 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -165,6 +165,7 @@ const ( HorizontalPodAutoscalerV1Beta1GeneratorName = "horizontalpodautoscaler/v1beta1" HorizontalPodAutoscalerV1GeneratorName = "horizontalpodautoscaler/v1" DeploymentV1Beta1GeneratorName = "deployment/v1beta1" + DeploymentBasicV1Beta1GeneratorName = "deployment-basic/v1beta1" JobV1Beta1GeneratorName = "job/v1beta1" JobV1GeneratorName = "job/v1" ScheduledJobV2Alpha1GeneratorName = "scheduledjob/v2alpha1" @@ -192,6 +193,9 @@ func DefaultGenerators(cmdName string) map[string]kubectl.Generator { generators["service-loadbalancer"] = map[string]kubectl.Generator{ ServiceLoadBalancerGeneratorV1Name: kubectl.ServiceLoadBalancerGeneratorV1{}, } + generators["deployment"] = map[string]kubectl.Generator{ + DeploymentBasicV1Beta1GeneratorName: kubectl.DeploymentBasicGeneratorV1{}, + } generators["run"] = map[string]kubectl.Generator{ RunV1GeneratorName: kubectl.BasicReplicationController{}, RunPodV1GeneratorName: kubectl.BasicPod{}, diff --git a/pkg/kubectl/deployment.go b/pkg/kubectl/deployment.go new file mode 100644 index 00000000000..035366585cf --- /dev/null +++ b/pkg/kubectl/deployment.go @@ -0,0 +1,107 @@ +/* +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 kubectl + +import ( + "fmt" + "strings" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/runtime" +) + +// DeploymentGeneratorV1 supports stable generation of a deployment +type DeploymentBasicGeneratorV1 struct { + Name string + Images []string +} + +// Ensure it supports the generator pattern that uses parameters specified during construction +var _ StructuredGenerator = &DeploymentBasicGeneratorV1{} + +func (DeploymentBasicGeneratorV1) ParamNames() []GeneratorParam { + return []GeneratorParam{ + {"name", true}, + {"image", true}, + } +} + +func (s DeploymentBasicGeneratorV1) 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 := &DeploymentBasicGeneratorV1{Name: name, Images: imageStrings} + return delegate.StructuredGenerate() +} + +// StructuredGenerate outputs a deployment object using the configured fields +func (s *DeploymentBasicGeneratorV1) StructuredGenerate() (runtime.Object, error) { + if err := s.validate(); err != nil { + return nil, err + } + + podSpec := api.PodSpec{Containers: []api.Container{}} + for _, imageString := range s.Images { + imageSplit := strings.Split(imageString, "/") + name := imageSplit[len(imageSplit)-1] + podSpec.Containers = append(podSpec.Containers, api.Container{Name: name, Image: imageString}) + } + + // setup default label and selector + labels := map[string]string{} + labels["app"] = s.Name + selector := unversioned.LabelSelector{MatchLabels: labels} + deployment := extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + Name: s.Name, + Labels: labels, + }, + Spec: extensions.DeploymentSpec{ + Replicas: 1, + Selector: &selector, + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: labels, + }, + Spec: podSpec, + }, + }, + } + return &deployment, nil +} + +// validate validates required fields are set to support structured generation +func (s *DeploymentBasicGeneratorV1) 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 new file mode 100644 index 00000000000..f538028ee17 --- /dev/null +++ b/pkg/kubectl/deployment_test.go @@ -0,0 +1,134 @@ +/* +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 kubectl + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" +) + +func TestDeploymentGenerate(t *testing.T) { + tests := []struct { + params map[string]interface{} + expected *extensions.Deployment + expectErr bool + }{ + { + params: map[string]interface{}{ + "name": "foo", + "image": []string{"abc/app:v4"}, + }, + expected: &extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"app": "foo"}, + }, + Spec: extensions.DeploymentSpec{ + Replicas: 1, + Selector: &unversioned.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}, + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"app": "foo"}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{{Name: "app:v4", Image: "abc/app:v4"}}, + }, + }, + }, + }, + expectErr: false, + }, + { + params: map[string]interface{}{ + "name": "foo", + "image": []string{"abc/app:v4", "zyx/ape"}, + }, + expected: &extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"app": "foo"}, + }, + Spec: extensions.DeploymentSpec{ + Replicas: 1, + Selector: &unversioned.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}, + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"app": "foo"}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{{Name: "app:v4", 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 := DeploymentBasicGeneratorV1{} + for index, test := range tests { + 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("%v: expected error and didn't get one", index) + continue // loop, no expected output object + case !test.expectErr && err != nil: + t.Errorf("%v: unexpected error %v", index, err) + continue // loop, no output object + case !test.expectErr && err == nil: + // do nothing and drop through + } + if !reflect.DeepEqual(obj.(*extensions.Deployment), test.expected) { + t.Errorf("%v\nexpected:\n%#v\nsaw:\n%#v", index, test.expected, obj.(*extensions.Deployment)) + } + } +}