diff --git a/hack/make-rules/test-cmd.sh b/hack/make-rules/test-cmd.sh index 160ae261f24..3197895da58 100755 --- a/hack/make-rules/test-cmd.sh +++ b/hack/make-rules/test-cmd.sh @@ -342,6 +342,7 @@ runTests() { deployment_image_field="(index .spec.template.spec.containers 0).image" deployment_second_image_field="(index .spec.template.spec.containers 1).image" change_cause_annotation='.*kubernetes.io/change-cause.*' + pdb_min_available=".spec.minAvailable" # Passing no arguments to create is an error ! kubectl create @@ -590,6 +591,16 @@ runTests() { # Post-condition: configmap exists and has expected values kube::test::get_object_assert 'configmap/test-configmap --namespace=test-kubectl-describe-pod' "{{$id_field}}" 'test-configmap' + ### Create a pod disruption budget + # Command + kubectl create pdb test-pdb --selector=app=rails --min-available=2 --namespace=test-kubectl-describe-pod + # Post-condition: pdb exists and has expected values + kube::test::get_object_assert 'pdb/test-pdb --namespace=test-kubectl-describe-pod' "{{$pdb_min_available}}" '2' + # Command + kubectl create pdb test-pdb-2 --selector=app=rails --min-available=50% --namespace=test-kubectl-describe-pod + # Post-condition: pdb exists and has expected values + kube::test::get_object_assert 'pdb/test-pdb-2 --namespace=test-kubectl-describe-pod' "{{$pdb_min_available}}" '50%' + # Create a pod that consumes secret, configmap, and downward API keys as envs kube::test::get_object_assert 'pods --namespace=test-kubectl-describe-pod' "{{range.items}}{{$id_field}}:{{end}}" '' kubectl create -f hack/testdata/pod-with-api-env.yaml --namespace=test-kubectl-describe-pod @@ -602,6 +613,7 @@ runTests() { kubectl delete pod env-test-pod --namespace=test-kubectl-describe-pod kubectl delete secret test-secret --namespace=test-kubectl-describe-pod kubectl delete configmap test-configmap --namespace=test-kubectl-describe-pod + kubectl delete pdb/test-pdb pdb/test-pdb-2 --namespace=test-kubectl-describe-pod kubectl delete namespace test-kubectl-describe-pod ### Create two PODs diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index fcf4f35ba9a..3b8c118ce26 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -388,6 +388,7 @@ mesos-launch-grace-period mesos-master mesos-sandbox-overlay mesos-user +min-available min-pr-number min-request-timeout min-resync-period diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 5452d848569..1ae04d7feb2 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -88,6 +88,7 @@ func NewCmdCreate(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { cmd.AddCommand(NewCmdCreateService(f, out, errOut)) cmd.AddCommand(NewCmdCreateDeployment(f, out)) cmd.AddCommand(NewCmdCreateClusterRoleBinding(f, out)) + cmd.AddCommand(NewCmdCreatePodDisruptionBudget(f, out)) return cmd } diff --git a/pkg/kubectl/cmd/create_pdb.go b/pkg/kubectl/cmd/create_pdb.go new file mode 100644 index 00000000000..599d3e7fbf0 --- /dev/null +++ b/pkg/kubectl/cmd/create_pdb.go @@ -0,0 +1,90 @@ +/* +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/spf13/cobra" + + "k8s.io/kubernetes/pkg/kubectl" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" +) + +var ( + pdbLong = templates.LongDesc(` + Create a pod disruption budget with the specified name, selector, and desired minimum available pods`) + + pdbExample = templates.Examples(` + # Create a pod disruption budget named my-pdb that will select all pods with the app=rails label + # and require at least one of them being available at any point in time. + kubectl create poddisruptionbudget my-pdb --selector=app=rails --min-available=1 + + # Create a pod disruption budget named my-pdb that will select all pods with the app=nginx label + # and require at least half of the pods selected to be available at any point in time. + kubectl create pdb my-pdb --selector=app=nginx --min-available=50%`) +) + +// NewCmdCreatePodDisruptionBudget is a macro command to create a new pod disruption budget. +func NewCmdCreatePodDisruptionBudget(f cmdutil.Factory, cmdOut io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "poddisruptionbudget NAME --selector=SELECTOR --min-available=N [--dry-run]", + Aliases: []string{"pdb"}, + Short: "Create a pod disruption budget with the specified name.", + Long: pdbLong, + Example: pdbExample, + Run: func(cmd *cobra.Command, args []string) { + err := CreatePodDisruptionBudget(f, cmdOut, cmd, args) + cmdutil.CheckErr(err) + }, + } + + cmdutil.AddApplyAnnotationFlags(cmd) + cmdutil.AddValidateFlags(cmd) + cmdutil.AddPrinterFlags(cmd) + cmdutil.AddGeneratorFlags(cmd, cmdutil.PodDisruptionBudgetV1GeneratorName) + cmd.Flags().String("min-available", "1", "The minimum number or percentage of available pods this budget requires.") + cmd.Flags().String("selector", "", "A label selector to use for this budget. Only equality-based selector requirements are supported.") + return cmd +} + +// CreatePodDisruptionBudget implements the behavior to run the create pdb command. +func CreatePodDisruptionBudget(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.PodDisruptionBudgetV1GeneratorName: + generator = &kubectl.PodDisruptionBudgetV1Generator{ + Name: name, + MinAvailable: cmdutil.GetFlagString(cmd, "min-available"), + Selector: cmdutil.GetFlagString(cmd, "selector"), + } + default: + return cmdutil.UsageError(cmd, fmt.Sprintf("Generator: %s not supported.", generatorName)) + } + return RunCreateSubcommand(f, cmd, cmdOut, &CreateSubcommandOptions{ + Name: name, + StructuredGenerator: generator, + DryRun: cmdutil.GetFlagBool(cmd, "dry-run"), + OutputFormat: cmdutil.GetFlagString(cmd, "output"), + }) +} diff --git a/pkg/kubectl/cmd/create_quota.go b/pkg/kubectl/cmd/create_quota.go index 1a23dba35b2..b319d057eca 100644 --- a/pkg/kubectl/cmd/create_quota.go +++ b/pkg/kubectl/cmd/create_quota.go @@ -33,10 +33,10 @@ var ( quotaExample = templates.Examples(` # Create a new resourcequota named my-quota - $ kubectl create quota my-quota --hard=cpu=1,memory=1G,pods=2,services=3,replicationcontrollers=2,resourcequotas=1,secrets=5,persistentvolumeclaims=10 + kubectl create quota my-quota --hard=cpu=1,memory=1G,pods=2,services=3,replicationcontrollers=2,resourcequotas=1,secrets=5,persistentvolumeclaims=10 # Create a new resourcequota named best-effort - $ kubectl create quota best-effort --hard=pods=100 --scopes=BestEffort`) + kubectl create quota best-effort --hard=pods=100 --scopes=BestEffort`) ) // NewCmdCreateQuota is a macro command to create a new quota diff --git a/pkg/kubectl/cmd/create_serviceaccount.go b/pkg/kubectl/cmd/create_serviceaccount.go index bad69ff96f3..898eb9b0f0e 100644 --- a/pkg/kubectl/cmd/create_serviceaccount.go +++ b/pkg/kubectl/cmd/create_serviceaccount.go @@ -33,7 +33,7 @@ var ( serviceAccountExample = templates.Examples(` # Create a new service account named my-service-account - $ kubectl create serviceaccount my-service-account`) + kubectl create serviceaccount my-service-account`) ) // NewCmdCreateServiceAccount is a macro command to create a new service account diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index a1976785060..d274aa5f072 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -211,6 +211,7 @@ const ( ConfigMapV1GeneratorName = "configmap/v1" ClusterRoleBindingV1GeneratorName = "clusterrolebinding.rbac.authorization.k8s.io/v1alpha1" ClusterV1Beta1GeneratorName = "cluster/v1beta1" + PodDisruptionBudgetV1GeneratorName = "poddisruptionbudget/v1beta1" ) // DefaultGenerators returns the set of default generators for use in Factory instances diff --git a/pkg/kubectl/pdb.go b/pkg/kubectl/pdb.go new file mode 100644 index 00000000000..ecc3b5b4846 --- /dev/null +++ b/pkg/kubectl/pdb.go @@ -0,0 +1,102 @@ +/* +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" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/policy" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/intstr" +) + +// PodDisruptionBudgetV1Generator supports stable generation of a pod disruption budget. +type PodDisruptionBudgetV1Generator struct { + Name string + MinAvailable string + Selector string +} + +// Ensure it supports the generator pattern that uses parameters specified during construction. +var _ StructuredGenerator = &PodDisruptionBudgetV1Generator{} + +func (PodDisruptionBudgetV1Generator) ParamNames() []GeneratorParam { + return []GeneratorParam{ + {"name", true}, + {"mim-available", true}, + {"selector", true}, + } +} + +func (s PodDisruptionBudgetV1Generator) 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) + } + minAvailable, isString := params["mim-available"].(string) + if !isString { + return nil, fmt.Errorf("expected string, found %v", minAvailable) + } + selector, isString := params["selecor"].(string) + if !isString { + return nil, fmt.Errorf("expected string, found %v", selector) + } + delegate := &PodDisruptionBudgetV1Generator{Name: name, MinAvailable: minAvailable, Selector: selector} + return delegate.StructuredGenerate() +} + +// StructuredGenerate outputs a pod disruption budget object using the configured fields. +func (s *PodDisruptionBudgetV1Generator) StructuredGenerate() (runtime.Object, error) { + if err := s.validate(); err != nil { + return nil, err + } + + selector, err := unversioned.ParseToLabelSelector(s.Selector) + if err != nil { + return nil, err + } + + return &policy.PodDisruptionBudget{ + ObjectMeta: api.ObjectMeta{ + Name: s.Name, + }, + Spec: policy.PodDisruptionBudgetSpec{ + MinAvailable: intstr.Parse(s.MinAvailable), + Selector: selector, + }, + }, nil +} + +// validate validates required fields are set to support structured generation. +func (s *PodDisruptionBudgetV1Generator) validate() error { + if len(s.Name) == 0 { + return fmt.Errorf("name must be specified") + } + if len(s.Selector) == 0 { + return fmt.Errorf("a selector must be specified") + } + if len(s.MinAvailable) == 0 { + return fmt.Errorf("the minimim number of available pods required must be specified") + } + return nil +} diff --git a/pkg/util/intstr/intstr.go b/pkg/util/intstr/intstr.go index 48f798ae505..adae8a4ffcb 100644 --- a/pkg/util/intstr/intstr.go +++ b/pkg/util/intstr/intstr.go @@ -70,6 +70,16 @@ func FromString(val string) IntOrString { return IntOrString{Type: String, StrVal: val} } +// Parse the given string and try to convert it to an integer before +// setting it as a string value. +func Parse(val string) IntOrString { + i, err := strconv.Atoi(val) + if err != nil { + return FromString(val) + } + return FromInt(i) +} + // UnmarshalJSON implements the json.Unmarshaller interface. func (intstr *IntOrString) UnmarshalJSON(value []byte) error { if value[0] == '"' {