diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD b/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD index 4ec52ba1e92..ba9d660af07 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD @@ -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", diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota.go b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota.go index c78546e953b..e5f639076c5 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota.go @@ -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 =,= +// 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 =", 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 } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota_test.go index 430e387976b..e700ec52dc5 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_quota_test.go @@ -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) } }) }