Merge pull request #28351 from sttts/sttts-kubectl-create-quota

Automatic merge from submit-queue

Add support for kubectl create quota command

Follow-up of https://github.com/kubernetes/kubernetes/pull/19625

```
Create a resourcequota with the specified name, hard limits and optional scopes

Usage:
  kubectl create quota NAME [--hard=key1=value1,key2=value2] [--scopes=Scope1,Scope2] [--dry-run=bool] [flags]

Aliases:
  quota, q


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

  // Create a new resourcequota named best-effort
  $ kubectl create quota best-effort --hard=pods=100 --scopes=BestEffort
```
pull/6/head
k8s-merge-robot 2016-07-26 21:20:04 -07:00 committed by GitHub
commit d82e404a00
11 changed files with 527 additions and 1 deletions

View File

@ -28,6 +28,7 @@ docs/man/man1/kubectl-convert.1
docs/man/man1/kubectl-cordon.1
docs/man/man1/kubectl-create-configmap.1
docs/man/man1/kubectl-create-namespace.1
docs/man/man1/kubectl-create-quota.1
docs/man/man1/kubectl-create-secret-docker-registry.1
docs/man/man1/kubectl-create-secret-generic.1
docs/man/man1/kubectl-create-secret-tls.1
@ -89,6 +90,7 @@ 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_namespace.md
docs/user-guide/kubectl/kubectl_create_quota.md
docs/user-guide/kubectl/kubectl_create_secret.md
docs/user-guide/kubectl/kubectl_create_secret_docker-registry.md
docs/user-guide/kubectl/kubectl_create_secret_generic.md

View File

@ -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.

View File

@ -0,0 +1,36 @@
<!-- BEGIN MUNGE: UNVERSIONED_WARNING -->
<!-- BEGIN STRIP_FOR_RELEASE -->
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<img src="http://kubernetes.io/kubernetes/img/warning.png" alt="WARNING"
width="25" height="25">
<h2>PLEASE NOTE: This document applies to the HEAD of the source tree</h2>
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).
</strong>
--
<!-- END STRIP_FOR_RELEASE -->
<!-- END MUNGE: UNVERSIONED_WARNING -->
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.
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_create_quota.md?pixel)]()
<!-- END MUNGE: GENERATED_ANALYTICS -->

View File

@ -196,6 +196,7 @@ grace-period
ha-domain
hairpin-mode
hard-pod-affinity-symmetric-weight
hard
healthz-bind-address
healthz-port
horizontal-pod-autoscaler-sync-period
@ -418,6 +419,7 @@ save-config
scheduler-config
scheduler-name
schema-cache-dir
scopes
seccomp-profile-root
secure-port
serialize-image-pulls

View File

@ -80,6 +80,7 @@ func NewCmdCreate(f *cmdutil.Factory, out io.Writer) *cobra.Command {
// create subcommands
cmd.AddCommand(NewCmdCreateNamespace(f, out))
cmd.AddCommand(NewCmdCreateQuota(f, out))
cmd.AddCommand(NewCmdCreateSecret(f, out))
cmd.AddCommand(NewCmdCreateConfigMap(f, out))
cmd.AddCommand(NewCmdCreateServiceAccount(f, out))

View File

@ -0,0 +1,86 @@
/*
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"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
)
const (
quotaLong = `
Create a resourcequota with the specified name, hard limits and optional scopes`
quotaExample = ` // 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
// Create a new resourcequota named best-effort
$ kubectl create quota best-effort --hard=pods=100 --scopes=BestEffort`
)
// NewCmdCreateQuota is a macro command to create a new quota
func NewCmdCreateQuota(f *cmdutil.Factory, cmdOut io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "quota NAME [--hard=key1=value1,key2=value2] [--scopes=Scope1,Scope2] [--dry-run=bool]",
Aliases: []string{"q"},
Short: "Create a quota with the specified name.",
Long: quotaLong,
Example: quotaExample,
Run: func(cmd *cobra.Command, args []string) {
err := CreateQuota(f, cmdOut, cmd, args)
cmdutil.CheckErr(err)
},
}
cmdutil.AddApplyAnnotationFlags(cmd)
cmdutil.AddValidateFlags(cmd)
cmdutil.AddPrinterFlags(cmd)
cmdutil.AddGeneratorFlags(cmd, cmdutil.ResourceQuotaV1GeneratorName)
cmd.Flags().String("hard", "", "A comma-delimited set of resource=quantity pairs that define a hard limit.")
cmd.Flags().String("scopes", "", "A comma-delimited set of quota scopes that must all match each object tracked by the quota.")
return cmd
}
// CreateQuota implements the behavior to run the create quota command
func CreateQuota(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.ResourceQuotaV1GeneratorName:
generator = &kubectl.ResourceQuotaGeneratorV1{
Name: name,
Hard: cmdutil.GetFlagString(cmd, "hard"),
Scopes: cmdutil.GetFlagString(cmd, "scopes"),
}
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"),
})
}

View File

@ -0,0 +1,79 @@
/*
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"
"net/http"
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/unversioned/fake"
)
func TestCreateQuota(t *testing.T) {
resourceQuotaObject := &api.ResourceQuota{}
resourceQuotaObject.Name = "my-quota"
f, tf, codec, ns := NewAPIFactory()
tf.Printer = &testPrinter{}
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == "/namespaces/test/resourcequotas" && m == "POST":
return &http.Response{StatusCode: 201, Header: defaultHeader(), Body: objBody(codec, resourceQuotaObject)}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
tests := map[string]struct {
flags map[string]string
expectedOutput string
}{
"single resource": {
flags: map[string]string{"hard": "cpu=1", "output": "name"},
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
},
"single resource with a scope": {
flags: map[string]string{"hard": "cpu=1", "output": "name", "scopes": "BestEffort"},
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
},
"multiple resources": {
flags: map[string]string{"hard": "cpu=1,pods=42", "output": "name", "scopes": "BestEffort"},
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
},
"single resource with multiple scopes": {
flags: map[string]string{"hard": "cpu=1", "output": "name", "scopes": "BestEffort,NotTerminating"},
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
},
}
for name, test := range tests {
buf := bytes.NewBuffer([]byte{})
cmd := NewCmdCreateQuota(f, buf)
cmd.Flags().Set("hard", "cpu=1")
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{resourceQuotaObject.Name})
if buf.String() != test.expectedOutput {
t.Errorf("%s: expected output: %s, but got: %s", name, test.expectedOutput, buf.String())
}
}
}

View File

@ -165,6 +165,7 @@ const (
JobV1Beta1GeneratorName = "job/v1beta1"
JobV1GeneratorName = "job/v1"
NamespaceV1GeneratorName = "namespace/v1"
ResourceQuotaV1GeneratorName = "resourcequotas/v1"
SecretV1GeneratorName = "secret/v1"
SecretForDockerRegistryV1GeneratorName = "secret-for-docker-registry/v1"
SecretForTLSV1GeneratorName = "secret-for-tls/v1"
@ -192,6 +193,11 @@ func DefaultGenerators(cmdName string) map[string]kubectl.Generator {
generators["namespace"] = map[string]kubectl.Generator{
NamespaceV1GeneratorName: kubectl.NamespaceGeneratorV1{},
}
generators["quota"] = map[string]kubectl.Generator{
ResourceQuotaV1GeneratorName: kubectl.ResourceQuotaGeneratorV1{},
}
generators["secret"] = map[string]kubectl.Generator{
SecretV1GeneratorName: kubectl.SecretGeneratorV1{},
}

125
pkg/kubectl/quota.go Normal file
View File

@ -0,0 +1,125 @@
/*
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/runtime"
)
// ResourceQuotaGeneratorV1 supports stable generation of a resource quota
type ResourceQuotaGeneratorV1 struct {
// The name of a quota object.
Name string
// The hard resource limit string before parsing.
Hard string
// The scopes of a quota object before parsing.
Scopes string
}
// ParamNames returns the set of supported input parameters when using the parameter injection generator pattern
func (g ResourceQuotaGeneratorV1) ParamNames() []GeneratorParam {
return []GeneratorParam{
{"name", true},
{"hard", true},
{"scopes", false},
}
}
// Ensure it supports the generator pattern that uses parameter injection
var _ Generator = &ResourceQuotaGeneratorV1{}
// Ensure it supports the generator pattern that uses parameters specified during construction
var _ StructuredGenerator = &ResourceQuotaGeneratorV1{}
func (g ResourceQuotaGeneratorV1) Generate(genericParams map[string]interface{}) (runtime.Object, error) {
err := ValidateParams(g.ParamNames(), genericParams)
if err != nil {
return nil, err
}
params := map[string]string{}
for key, value := range genericParams {
strVal, isString := value.(string)
if !isString {
return nil, fmt.Errorf("expected string, saw %v for '%s'", value, key)
}
params[key] = strVal
}
delegate := &ResourceQuotaGeneratorV1{}
delegate.Name = params["name"]
delegate.Hard = params["hard"]
delegate.Scopes = params["scopes"]
return delegate.StructuredGenerate()
}
// StructuredGenerate outputs a ResourceQuota object using the configured fields
func (g *ResourceQuotaGeneratorV1) StructuredGenerate() (runtime.Object, error) {
if err := g.validate(); err != nil {
return nil, err
}
resourceList, err := populateResourceList(g.Hard)
if err != nil {
return nil, err
}
scopes, err := parseScopes(g.Scopes)
if err != nil {
return nil, err
}
resourceQuota := &api.ResourceQuota{}
resourceQuota.Name = g.Name
resourceQuota.Spec.Hard = resourceList
resourceQuota.Spec.Scopes = scopes
return resourceQuota, nil
}
// validate validates required fields are set to support structured generation
func (r *ResourceQuotaGeneratorV1) validate() error {
if len(r.Name) == 0 {
return fmt.Errorf("name must be specified")
}
return nil
}
func parseScopes(spec string) ([]api.ResourceQuotaScope, error) {
// empty input gets a nil response to preserve generator test expected behaviors
if spec == "" {
return nil, nil
}
scopes := strings.Split(spec, ",")
result := make([]api.ResourceQuotaScope, 0, len(scopes))
for _, scope := range scopes {
// intentionally do not verify the scope against the valid scope list. This is done by the apiserver anyway.
if scope == "" {
return nil, fmt.Errorf("invalid resource quota scope \"\"")
}
result = append(result, api.ResourceQuotaScope(scope))
}
return result, nil
}

114
pkg/kubectl/quota_test.go Normal file
View File

@ -0,0 +1,114 @@
/*
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"
)
func TestQuotaGenerate(t *testing.T) {
hard := "cpu=10,memory=5G,pods=10,services=7"
resourceQuotaSpecList, err := populateResourceList(hard)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
tests := map[string]struct {
params map[string]interface{}
expected *api.ResourceQuota
expectErr bool
}{
"test-valid-use": {
params: map[string]interface{}{
"name": "foo",
"hard": hard,
},
expected: &api.ResourceQuota{
ObjectMeta: api.ObjectMeta{
Name: "foo",
},
Spec: api.ResourceQuotaSpec{Hard: resourceQuotaSpecList},
},
expectErr: false,
},
"test-missing-required-param": {
params: map[string]interface{}{
"name": "foo",
},
expectErr: true,
},
"test-valid-scopes": {
params: map[string]interface{}{
"name": "foo",
"hard": hard,
"scopes": "BestEffort,NotTerminating",
},
expected: &api.ResourceQuota{
ObjectMeta: api.ObjectMeta{
Name: "foo",
},
Spec: api.ResourceQuotaSpec{
Hard: resourceQuotaSpecList,
Scopes: []api.ResourceQuotaScope{
api.ResourceQuotaScopeBestEffort,
api.ResourceQuotaScopeNotTerminating,
},
},
},
expectErr: false,
},
"test-empty-scopes": {
params: map[string]interface{}{
"name": "foo",
"hard": hard,
"scopes": "",
},
expected: &api.ResourceQuota{
ObjectMeta: api.ObjectMeta{
Name: "foo",
},
Spec: api.ResourceQuotaSpec{Hard: resourceQuotaSpecList},
},
expectErr: false,
},
"test-invalid-scopes": {
params: map[string]interface{}{
"name": "foo",
"hard": hard,
"scopes": "abc,",
},
expectErr: true,
},
}
generator := ResourceQuotaGeneratorV1{}
for name, test := range tests {
obj, err := generator.Generate(test.params)
if !test.expectErr && err != nil {
t.Errorf("%s: unexpected error: %v", name, err)
}
if test.expectErr && err != nil {
continue
}
if !reflect.DeepEqual(obj.(*api.ResourceQuota), test.expected) {
t.Errorf("%s:\nexpected:\n%#v\nsaw:\n%#v", name, test.expected, obj.(*api.ResourceQuota))
}
}
}

View File

@ -44,6 +44,7 @@ import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/annotations"
apierrs "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/api/unversioned"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/controller"
@ -1089,10 +1090,11 @@ var _ = framework.KubeDescribe("Kubectl client", func() {
})
framework.KubeDescribe("Kubectl run --rm job", func() {
nsFlag := fmt.Sprintf("--namespace=%v", ns)
jobName := "e2e-test-rm-busybox-job"
It("should create a job from an image, then delete the job [Conformance]", func() {
nsFlag := fmt.Sprintf("--namespace=%v", ns)
// The rkt runtime doesn't support attach, see #23335
framework.SkipIfContainerRuntimeIs("rkt")
framework.SkipUnlessServerVersionGTE(jobsVersion, c)
@ -1197,6 +1199,76 @@ var _ = framework.KubeDescribe("Kubectl client", func() {
})
})
framework.KubeDescribe("Kubectl create quota", func() {
It("should create a quota without scopes", func() {
nsFlag := fmt.Sprintf("--namespace=%v", ns)
quotaName := "million"
By("calling kubectl quota")
framework.RunKubectlOrDie("create", "quota", quotaName, "--hard=pods=1000000,services=1000000", nsFlag)
By("verifying that the quota was created")
quota, err := c.ResourceQuotas(ns).Get(quotaName)
if err != nil {
framework.Failf("Failed getting quota %s: %v", quotaName, err)
}
if len(quota.Spec.Scopes) != 0 {
framework.Failf("Expected empty scopes, got %v", quota.Spec.Scopes)
}
if len(quota.Spec.Hard) != 2 {
framework.Failf("Expected two resources, got %v", quota.Spec.Hard)
}
r, found := quota.Spec.Hard[api.ResourcePods]
if expected := resource.MustParse("1000000"); !found || (&r).Cmp(expected) != 0 {
framework.Failf("Expected pods=1000000, got %v", r)
}
r, found = quota.Spec.Hard[api.ResourceServices]
if expected := resource.MustParse("1000000"); !found || (&r).Cmp(expected) != 0 {
framework.Failf("Expected services=1000000, got %v", r)
}
})
It("should create a quota with scopes", func() {
nsFlag := fmt.Sprintf("--namespace=%v", ns)
quotaName := "scopes"
By("calling kubectl quota")
framework.RunKubectlOrDie("create", "quota", quotaName, "--hard=pods=1000000", "--scopes=BestEffort,NotTerminating", nsFlag)
By("verifying that the quota was created")
quota, err := c.ResourceQuotas(ns).Get(quotaName)
if err != nil {
framework.Failf("Failed getting quota %s: %v", quotaName, err)
}
if len(quota.Spec.Scopes) != 2 {
framework.Failf("Expected two scopes, got %v", quota.Spec.Scopes)
}
scopes := make(map[api.ResourceQuotaScope]struct{})
for _, scope := range quota.Spec.Scopes {
scopes[scope] = struct{}{}
}
if _, found := scopes[api.ResourceQuotaScopeBestEffort]; !found {
framework.Failf("Expected BestEffort scope, got %v", quota.Spec.Scopes)
}
if _, found := scopes[api.ResourceQuotaScopeNotTerminating]; !found {
framework.Failf("Expected NotTerminating scope, got %v", quota.Spec.Scopes)
}
})
It("should reject quota with invalid scopes", func() {
nsFlag := fmt.Sprintf("--namespace=%v", ns)
quotaName := "scopes"
By("calling kubectl quota")
out, err := framework.RunKubectl("create", "quota", quotaName, "--hard=hard=pods=1000000", "--scopes=Foo", nsFlag)
if err == nil {
framework.Failf("Expected kubectl to fail, but it succeeded: %s", out)
}
})
})
})
// Checks whether the output split by line contains the required elements.