mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 20:24:09 +00:00
Remove the dependency between create quota command and generators
This commit is contained in:
parent
def8fe3b4e
commit
0222f2d033
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user