Remove the dependency between create quota command and generators

This commit is contained in:
Sai Harsha Kottapalli 2020-10-03 03:04:09 +05:30
parent def8fe3b4e
commit 0222f2d033
No known key found for this signature in database
GPG Key ID: 4A710869A1A388D5
3 changed files with 297 additions and 55 deletions

View File

@ -30,6 +30,7 @@ go_library(
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/api/rbac/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
@ -43,6 +44,7 @@ go_library(
"//staging/src/k8s.io/client-go/kubernetes/typed/apps/v1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/batch/v1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/batch/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/rbac/v1:go_default_library",
"//staging/src/k8s.io/component-base/cli/flag:go_default_library",
"//staging/src/k8s.io/kubectl/pkg/cmd/util:go_default_library",

View File

@ -17,12 +17,22 @@ limitations under the License.
package create
import (
"context"
"fmt"
"strings"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
resourceapi "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
resourcecli "k8s.io/cli-runtime/pkg/resource"
coreclient "k8s.io/client-go/kubernetes/typed/core/v1"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/generate"
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
"k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
)
@ -41,14 +51,38 @@ var (
// QuotaOpts holds the command-line options for 'create quota' sub command
type QuotaOpts struct {
CreateSubcommandOptions *CreateSubcommandOptions
// PrintFlags holds options necessary for obtaining a printer
PrintFlags *genericclioptions.PrintFlags
PrintObj func(obj runtime.Object) error
// 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
CreateAnnotation bool
FieldManager string
Namespace string
EnforceNamespace bool
Client *coreclient.CoreV1Client
DryRunStrategy cmdutil.DryRunStrategy
DryRunVerifier *resourcecli.DryRunVerifier
genericclioptions.IOStreams
}
// NewQuotaOpts creates a new *QuotaOpts with sane defaults
func NewQuotaOpts(ioStreams genericclioptions.IOStreams) *QuotaOpts {
return &QuotaOpts{
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
IOStreams: ioStreams,
}
}
// NewCmdCreateQuota is a macro command to create a new quota
func NewCmdCreateQuota(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
options := &QuotaOpts{
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
}
o := NewQuotaOpts(ioStreams)
cmd := &cobra.Command{
Use: "quota NAME [--hard=key1=value1,key2=value2] [--scopes=Scope1,Scope2] [--dry-run=server|client|none]",
@ -58,45 +92,184 @@ func NewCmdCreateQuota(f cmdutil.Factory, ioStreams genericclioptions.IOStreams)
Long: quotaLong,
Example: quotaExample,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(options.Complete(f, cmd, args))
cmdutil.CheckErr(options.Run())
cmdutil.CheckErr(o.Complete(f, cmd, args))
cmdutil.CheckErr(o.Validate())
cmdutil.CheckErr(o.Run())
},
}
options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
o.PrintFlags.AddFlags(cmd)
cmdutil.AddApplyAnnotationFlags(cmd)
cmdutil.AddValidateFlags(cmd)
cmdutil.AddGeneratorFlags(cmd, generateversioned.ResourceQuotaV1GeneratorName)
cmd.Flags().String("hard", "", i18n.T("A comma-delimited set of resource=quantity pairs that define a hard limit."))
cmd.Flags().String("scopes", "", i18n.T("A comma-delimited set of quota scopes that must all match each object tracked by the quota."))
cmdutil.AddFieldManagerFlagVar(cmd, &options.CreateSubcommandOptions.FieldManager, "kubectl-create")
cmdutil.AddDryRunFlag(cmd)
cmd.Flags().StringVar(&o.Hard, "hard", o.Hard, i18n.T("A comma-delimited set of resource=quantity pairs that define a hard limit."))
cmd.Flags().StringVar(&o.Scopes, "scopes", o.Scopes, i18n.T("A comma-delimited set of quota scopes that must all match each object tracked by the quota."))
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
return cmd
}
// Complete completes all the required options
func (o *QuotaOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
name, err := NameFromCommandArgs(cmd, args)
var err error
o.Name, err = NameFromCommandArgs(cmd, args)
if err != nil {
return err
}
var generator generate.StructuredGenerator
switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName {
case generateversioned.ResourceQuotaV1GeneratorName:
generator = &generateversioned.ResourceQuotaGeneratorV1{
Name: name,
Hard: cmdutil.GetFlagString(cmd, "hard"),
Scopes: cmdutil.GetFlagString(cmd, "scopes"),
}
default:
return errUnsupportedGenerator(cmd, generatorName)
restConfig, err := f.ToRESTConfig()
if err != nil {
return err
}
o.Client, err = coreclient.NewForConfig(restConfig)
if err != nil {
return err
}
return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
if err != nil {
return err
}
dynamicClient, err := f.DynamicClient()
if err != nil {
return err
}
discoveryClient, err := f.ToDiscoveryClient()
if err != nil {
return err
}
o.DryRunVerifier = resourcecli.NewDryRunVerifier(dynamicClient, discoveryClient)
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
if err != nil {
return err
}
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
printer, err := o.PrintFlags.ToPrinter()
if err != nil {
return err
}
o.PrintObj = func(obj runtime.Object) error {
return printer.PrintObj(obj, o.Out)
}
return nil
}
// Run calls the CreateSubcommandOptions.Run in QuotaOpts instance
func (o *QuotaOpts) Run() error {
return o.CreateSubcommandOptions.Run()
// Validate checks to the QuotaOpts to see if there is sufficient information run the command.
func (o *QuotaOpts) Validate() error {
if len(o.Name) == 0 {
return fmt.Errorf("name must be specified")
}
return nil
}
// Run does the work
func (o *QuotaOpts) Run() error {
resourceQuota, err := o.createQuota()
if err != nil {
return err
}
if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, resourceQuota, scheme.DefaultJSONEncoder()); err != nil {
return err
}
if o.DryRunStrategy != cmdutil.DryRunClient {
createOptions := metav1.CreateOptions{}
if o.FieldManager != "" {
createOptions.FieldManager = o.FieldManager
}
if o.DryRunStrategy == cmdutil.DryRunServer {
if err := o.DryRunVerifier.HasSupport(resourceQuota.GroupVersionKind()); err != nil {
return err
}
createOptions.DryRun = []string{metav1.DryRunAll}
}
resourceQuota, err = o.Client.ResourceQuotas(o.Namespace).Create(context.TODO(), resourceQuota, createOptions)
if err != nil {
return fmt.Errorf("failed to create quota: %v", err)
}
}
return o.PrintObj(resourceQuota)
}
func (o *QuotaOpts) createQuota() (*corev1.ResourceQuota, error) {
namespace := ""
if o.EnforceNamespace {
namespace = o.Namespace
}
fmt.Println(corev1.SchemeGroupVersion.String())
resourceQuota := &corev1.ResourceQuota{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String(), Kind: "ResourceQuota"},
ObjectMeta: metav1.ObjectMeta{
Name: o.Name,
Namespace: namespace,
},
}
resourceList, err := populateResourceListV1(o.Hard)
if err != nil {
return nil, err
}
scopes, err := parseScopes(o.Scopes)
if err != nil {
return nil, err
}
resourceQuota.Spec.Hard = resourceList
resourceQuota.Spec.Scopes = scopes
return resourceQuota, nil
}
// populateResourceListV1 takes strings of form <resourceName1>=<value1>,<resourceName1>=<value2>
// and returns ResourceList.
func populateResourceListV1(spec string) (corev1.ResourceList, error) {
// empty input gets a nil response to preserve generator test expected behaviors
if spec == "" {
return nil, nil
}
result := corev1.ResourceList{}
resourceStatements := strings.Split(spec, ",")
for _, resourceStatement := range resourceStatements {
parts := strings.Split(resourceStatement, "=")
if len(parts) != 2 {
return nil, fmt.Errorf("Invalid argument syntax %v, expected <resource>=<value>", resourceStatement)
}
resourceName := corev1.ResourceName(parts[0])
resourceQuantity, err := resourceapi.ParseQuantity(parts[1])
if err != nil {
return nil, err
}
result[resourceName] = resourceQuantity
}
return result, nil
}
func parseScopes(spec string) ([]corev1.ResourceQuotaScope, error) {
// empty input gets a nil response to preserve test expected behaviors
if spec == "" {
return nil, nil
}
scopes := strings.Split(spec, ",")
result := make([]corev1.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, corev1.ResourceQuotaScope(scope))
}
return result, nil
}

View File

@ -19,49 +19,116 @@ package create
import (
"testing"
"k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestCreateQuota(t *testing.T) {
resourceQuotaObject := &v1.ResourceQuota{}
resourceQuotaObject.Name = "my-quota"
hards := []string{"cpu=1", "cpu=1,pods=42"}
var resourceQuotaSpecLists []corev1.ResourceList
for _, hard := range hards {
resourceQuotaSpecList, err := populateResourceListV1(hard)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
resourceQuotaSpecLists = append(resourceQuotaSpecLists, resourceQuotaSpecList)
}
tests := map[string]struct {
flags []string
expectedOutput string
options *QuotaOpts
expected *corev1.ResourceQuota
}{
"single resource": {
flags: []string{"--hard=cpu=1"},
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
options: &QuotaOpts{
Name: "my-quota",
Hard: hards[0],
Scopes: "",
},
expected: &corev1.ResourceQuota{
TypeMeta: metav1.TypeMeta{
Kind: "ResourceQuota",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-quota",
},
Spec: corev1.ResourceQuotaSpec{
Hard: resourceQuotaSpecLists[0],
},
},
},
"single resource with a scope": {
flags: []string{"--hard=cpu=1", "--scopes=BestEffort"},
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
options: &QuotaOpts{
Name: "my-quota",
Hard: hards[0],
Scopes: "BestEffort",
},
expected: &corev1.ResourceQuota{
TypeMeta: metav1.TypeMeta{
Kind: "ResourceQuota",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-quota",
},
Spec: corev1.ResourceQuotaSpec{
Hard: resourceQuotaSpecLists[0],
Scopes: []corev1.ResourceQuotaScope{"BestEffort"},
},
},
},
"multiple resources": {
flags: []string{"--hard=cpu=1,pods=42", "--scopes=BestEffort"},
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
options: &QuotaOpts{
Name: "my-quota",
Hard: hards[1],
Scopes: "BestEffort",
},
expected: &corev1.ResourceQuota{
TypeMeta: metav1.TypeMeta{
Kind: "ResourceQuota",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-quota",
},
Spec: corev1.ResourceQuotaSpec{
Hard: resourceQuotaSpecLists[1],
Scopes: []corev1.ResourceQuotaScope{"BestEffort"},
},
},
},
"single resource with multiple scopes": {
flags: []string{"--hard=cpu=1", "--scopes=BestEffort,NotTerminating"},
expectedOutput: "resourcequota/" + resourceQuotaObject.Name + "\n",
options: &QuotaOpts{
Name: "my-quota",
Hard: hards[0],
Scopes: "BestEffort,NotTerminating",
},
expected: &corev1.ResourceQuota{
TypeMeta: metav1.TypeMeta{
Kind: "ResourceQuota",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-quota",
},
Spec: corev1.ResourceQuotaSpec{
Hard: resourceQuotaSpecLists[0],
Scopes: []corev1.ResourceQuotaScope{"BestEffort", "NotTerminating"},
},
},
},
}
for name, test := range tests {
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams()
cmd := NewCmdCreateQuota(tf, ioStreams)
cmd.Flags().Parse(test.flags)
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())
resourceQuota, err := tc.options.createQuota()
if err != nil {
t.Errorf("unexpected error:\n%#v\n", err)
return
}
if !apiequality.Semantic.DeepEqual(resourceQuota, tc.expected) {
t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, resourceQuota)
}
})
}