From 12a58f3f15a428111d8053a577ac3d21d229a81b Mon Sep 17 00:00:00 2001 From: Martin Schimandl Date: Fri, 2 Oct 2020 18:16:28 +0200 Subject: [PATCH] remove generator from service in kubectl --- .../src/k8s.io/kubectl/pkg/cmd/create/BUILD | 3 + .../kubectl/pkg/cmd/create/create_service.go | 413 ++++++++++-------- .../pkg/cmd/create/create_service_test.go | 343 ++++++++++----- .../pkg/cmd/create/create_serviceaccount.go | 4 + 4 files changed, 486 insertions(+), 277 deletions(-) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD b/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD index 037f83292ee..d58150fb1cb 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD @@ -41,6 +41,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/util/intstr:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/printers:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/resource:go_default_library", @@ -64,6 +65,7 @@ go_library( "//staging/src/k8s.io/kubectl/pkg/util/templates:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/k8s.io/klog/v2:go_default_library", + "//vendor/k8s.io/utils/net:go_default_library", ], ) @@ -101,6 +103,7 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/intstr:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/resource:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library", diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service.go b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service.go index 4cede960557..8661c712df8 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service.go @@ -17,15 +17,27 @@ limitations under the License. package create import ( + "context" + "fmt" + "strconv" + "strings" + "github.com/spf13/cobra" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + corev1client "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" + utilsnet "k8s.io/utils/net" ) // NewCmdCreateService is a macro command to create a new service @@ -45,6 +57,172 @@ func NewCmdCreateService(f cmdutil.Factory, ioStreams genericclioptions.IOStream return cmd } +// ServiceOptions holds the options for 'create service' sub command +type ServiceOptions struct { + PrintFlags *genericclioptions.PrintFlags + PrintObj func(obj runtime.Object) error + + Name string + TCP []string + Type corev1.ServiceType + ClusterIP string + NodePort int + ExternalName string + + FieldManager string + CreateAnnotation bool + Namespace string + + Client corev1client.CoreV1Interface + DryRunStrategy cmdutil.DryRunStrategy + DryRunVerifier *resource.DryRunVerifier + genericclioptions.IOStreams +} + +// NewServiceOptions creates a ServiceOptions struct +func NewServiceOptions(ioStreams genericclioptions.IOStreams, serviceType corev1.ServiceType) *ServiceOptions { + return &ServiceOptions{ + PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), + IOStreams: ioStreams, + Type: serviceType, + } +} + +// Complete completes all the required options +func (o *ServiceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + name, err := NameFromCommandArgs(cmd, args) + if err != nil { + return err + } + o.Name = name + + clientConfig, err := f.ToRESTConfig() + if err != nil { + return err + } + o.Client, err = corev1client.NewForConfig(clientConfig) + if err != nil { + return err + } + + o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + 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 = resource.NewDryRunVerifier(dynamicClient, discoveryClient) + 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 +} + +// Validate if the options are valid +func (o *ServiceOptions) Validate() error { + if o.ClusterIP == corev1.ClusterIPNone && o.Type != corev1.ServiceTypeClusterIP { + return fmt.Errorf("ClusterIP=None can only be used with ClusterIP service type") + } + if o.ClusterIP != corev1.ClusterIPNone && len(o.TCP) == 0 && o.Type != corev1.ServiceTypeExternalName { + return fmt.Errorf("at least one tcp port specifier must be provided") + } + if o.Type == corev1.ServiceTypeExternalName { + if errs := validation.IsDNS1123Subdomain(o.ExternalName); len(errs) != 0 { + return fmt.Errorf("invalid service external name %s", o.ExternalName) + } + } + return nil +} + +func (o *ServiceOptions) createService() (*corev1.Service, error) { + ports := []corev1.ServicePort{} + for _, tcpString := range o.TCP { + port, targetPort, err := parsePorts(tcpString) + if err != nil { + return nil, err + } + + portName := strings.Replace(tcpString, ":", "-", -1) + ports = append(ports, corev1.ServicePort{ + Name: portName, + Port: port, + TargetPort: targetPort, + Protocol: corev1.Protocol("TCP"), + NodePort: int32(o.NodePort), + }) + } + + // setup default label and selector + labels := map[string]string{} + labels["app"] = o.Name + selector := map[string]string{} + selector["app"] = o.Name + + service := corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: o.Name, + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceType(o.Type), + Selector: selector, + Ports: ports, + ExternalName: o.ExternalName, + }, + } + if len(o.ClusterIP) > 0 { + service.Spec.ClusterIP = o.ClusterIP + } + return &service, nil +} + +// Run the service command +func (o *ServiceOptions) Run() error { + service, err := o.createService() + if err != nil { + return err + } + + if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, service, 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 { + createOptions.DryRun = []string{metav1.DryRunAll} + } + var err error + service, err = o.Client.Services(o.Namespace).Create(context.TODO(), service, createOptions) + if err != nil { + return fmt.Errorf("failed to create %s service: %v", o.Type, err) + } + } + return o.PrintObj(service) +} + var ( serviceClusterIPLong = templates.LongDesc(i18n.T(` Create a ClusterIP service with the specified name.`)) @@ -57,20 +235,9 @@ var ( kubectl create service clusterip my-cs --clusterip="None"`)) ) -func addPortFlags(cmd *cobra.Command) { - cmd.Flags().StringSlice("tcp", []string{}, "Port pairs can be specified as ':'.") -} - -// ServiceClusterIPOpts holds the options for 'create service clusterip' sub command -type ServiceClusterIPOpts struct { - CreateSubcommandOptions *CreateSubcommandOptions -} - // NewCmdCreateServiceClusterIP is a command to create a ClusterIP service func NewCmdCreateServiceClusterIP(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - options := &ServiceClusterIPOpts{ - CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams), - } + o := NewServiceOptions(ioStreams, corev1.ServiceTypeClusterIP) cmd := &cobra.Command{ Use: "clusterip NAME [--tcp=:] [--dry-run=server|client|none]", @@ -79,54 +246,24 @@ func NewCmdCreateServiceClusterIP(f cmdutil.Factory, ioStreams genericclioptions Long: serviceClusterIPLong, Example: serviceClusterIPExample, 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.ServiceClusterIPGeneratorV1Name) - addPortFlags(cmd) - cmd.Flags().String("clusterip", "", i18n.T("Assign your own ClusterIP or set to 'None' for a 'headless' service (no loadbalancing).")) - cmdutil.AddFieldManagerFlagVar(cmd, &options.CreateSubcommandOptions.FieldManager, "kubectl-create") + cmd.Flags().StringSliceVar(&o.TCP, "tcp", o.TCP, "Port pairs can be specified as ':'.") + cmd.Flags().StringVar(&o.ClusterIP, "clusterip", o.ClusterIP, i18n.T("Assign your own ClusterIP or set to 'None' for a 'headless' service (no loadbalancing).")) + cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") + cmdutil.AddDryRunFlag(cmd) + return cmd } -func errUnsupportedGenerator(cmd *cobra.Command, generatorName string) error { - return cmdutil.UsageErrorf(cmd, "Generator %s not supported. ", generatorName) -} - -// Complete completes all the required options -func (o *ServiceClusterIPOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { - name, err := NameFromCommandArgs(cmd, args) - if err != nil { - return err - } - - var generator generate.StructuredGenerator - switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { - case generateversioned.ServiceClusterIPGeneratorV1Name: - generator = &generateversioned.ServiceCommonGeneratorV1{ - Name: name, - TCP: cmdutil.GetFlagStringSlice(cmd, "tcp"), - Type: v1.ServiceTypeClusterIP, - ClusterIP: cmdutil.GetFlagString(cmd, "clusterip"), - } - default: - return errUnsupportedGenerator(cmd, generatorName) - } - - return o.CreateSubcommandOptions.Complete(f, cmd, args, generator) -} - -// Run calls the CreateSubcommandOptions.Run in ServiceClusterIPOpts instance -func (o *ServiceClusterIPOpts) Run() error { - return o.CreateSubcommandOptions.Run() -} - var ( serviceNodePortLong = templates.LongDesc(i18n.T(` Create a NodePort service with the specified name.`)) @@ -136,16 +273,9 @@ var ( kubectl create service nodeport my-ns --tcp=5678:8080`)) ) -// ServiceNodePortOpts holds the options for 'create service nodeport' sub command -type ServiceNodePortOpts struct { - CreateSubcommandOptions *CreateSubcommandOptions -} - // NewCmdCreateServiceNodePort is a macro command for creating a NodePort service func NewCmdCreateServiceNodePort(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - options := &ServiceNodePortOpts{ - CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams), - } + o := NewServiceOptions(ioStreams, corev1.ServiceTypeNodePort) cmd := &cobra.Command{ Use: "nodeport NAME [--tcp=port:targetPort] [--dry-run=server|client|none]", @@ -154,51 +284,23 @@ func NewCmdCreateServiceNodePort(f cmdutil.Factory, ioStreams genericclioptions. Long: serviceNodePortLong, Example: serviceNodePortExample, 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.ServiceNodePortGeneratorV1Name) - cmd.Flags().Int("node-port", 0, "Port used to expose the service on each node in a cluster.") - cmdutil.AddFieldManagerFlagVar(cmd, &options.CreateSubcommandOptions.FieldManager, "kubectl-create") - addPortFlags(cmd) + cmd.Flags().IntVar(&o.NodePort, "node-port", o.NodePort, "Port used to expose the service on each node in a cluster.") + cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") + cmd.Flags().StringSliceVar(&o.TCP, "tcp", o.TCP, "Port pairs can be specified as ':'.") + cmdutil.AddDryRunFlag(cmd) return cmd } -// Complete completes all the required options -func (o *ServiceNodePortOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { - name, err := NameFromCommandArgs(cmd, args) - if err != nil { - return err - } - - var generator generate.StructuredGenerator - switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { - case generateversioned.ServiceNodePortGeneratorV1Name: - generator = &generateversioned.ServiceCommonGeneratorV1{ - Name: name, - TCP: cmdutil.GetFlagStringSlice(cmd, "tcp"), - Type: v1.ServiceTypeNodePort, - ClusterIP: "", - NodePort: cmdutil.GetFlagInt(cmd, "node-port"), - } - default: - return errUnsupportedGenerator(cmd, generatorName) - } - - return o.CreateSubcommandOptions.Complete(f, cmd, args, generator) -} - -// Run calls the CreateSubcommandOptions.Run in ServiceNodePortOpts instance -func (o *ServiceNodePortOpts) Run() error { - return o.CreateSubcommandOptions.Run() -} - var ( serviceLoadBalancerLong = templates.LongDesc(i18n.T(` Create a LoadBalancer service with the specified name.`)) @@ -208,16 +310,9 @@ var ( kubectl create service loadbalancer my-lbs --tcp=5678:8080`)) ) -// ServiceLoadBalancerOpts holds the options for 'create service loadbalancer' sub command -type ServiceLoadBalancerOpts struct { - CreateSubcommandOptions *CreateSubcommandOptions -} - // NewCmdCreateServiceLoadBalancer is a macro command for creating a LoadBalancer service func NewCmdCreateServiceLoadBalancer(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - options := &ServiceLoadBalancerOpts{ - CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams), - } + o := NewServiceOptions(ioStreams, corev1.ServiceTypeLoadBalancer) cmd := &cobra.Command{ Use: "loadbalancer NAME [--tcp=port:targetPort] [--dry-run=server|client|none]", @@ -226,49 +321,22 @@ func NewCmdCreateServiceLoadBalancer(f cmdutil.Factory, ioStreams genericcliopti Long: serviceLoadBalancerLong, Example: serviceLoadBalancerExample, 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.ServiceLoadBalancerGeneratorV1Name) - cmdutil.AddFieldManagerFlagVar(cmd, &options.CreateSubcommandOptions.FieldManager, "kubectl-create") - addPortFlags(cmd) + cmd.Flags().StringSliceVar(&o.TCP, "tcp", o.TCP, "Port pairs can be specified as ':'.") + cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") + cmdutil.AddDryRunFlag(cmd) return cmd } -// Complete completes all the required options -func (o *ServiceLoadBalancerOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { - name, err := NameFromCommandArgs(cmd, args) - if err != nil { - return err - } - - var generator generate.StructuredGenerator - switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { - case generateversioned.ServiceLoadBalancerGeneratorV1Name: - generator = &generateversioned.ServiceCommonGeneratorV1{ - Name: name, - TCP: cmdutil.GetFlagStringSlice(cmd, "tcp"), - Type: v1.ServiceTypeLoadBalancer, - ClusterIP: "", - } - default: - return errUnsupportedGenerator(cmd, generatorName) - } - - return o.CreateSubcommandOptions.Complete(f, cmd, args, generator) -} - -// Run calls the CreateSubcommandOptions.Run in ServiceLoadBalancerOpts instance -func (o *ServiceLoadBalancerOpts) Run() error { - return o.CreateSubcommandOptions.Run() -} - var ( serviceExternalNameLong = templates.LongDesc(i18n.T(` Create an ExternalName service with the specified name. @@ -282,16 +350,9 @@ var ( kubectl create service externalname my-ns --external-name bar.com`)) ) -// ServiceExternalNameOpts holds the options for 'create service externalname' sub command -type ServiceExternalNameOpts struct { - CreateSubcommandOptions *CreateSubcommandOptions -} - // NewCmdCreateServiceExternalName is a macro command for creating an ExternalName service func NewCmdCreateServiceExternalName(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - options := &ServiceExternalNameOpts{ - CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams), - } + o := NewServiceOptions(ioStreams, corev1.ServiceTypeExternalName) cmd := &cobra.Command{ Use: "externalname NAME --external-name external.name [--dry-run=server|client|none]", @@ -300,47 +361,47 @@ func NewCmdCreateServiceExternalName(f cmdutil.Factory, ioStreams genericcliopti Long: serviceExternalNameLong, Example: serviceExternalNameExample, 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.ServiceExternalNameGeneratorV1Name) - addPortFlags(cmd) - cmd.Flags().String("external-name", "", i18n.T("External name of service")) + cmd.Flags().StringSliceVar(&o.TCP, "tcp", o.TCP, "Port pairs can be specified as ':'.") + cmd.Flags().StringVar(&o.ExternalName, "external-name", o.ExternalName, i18n.T("External name of service")) cmd.MarkFlagRequired("external-name") - cmdutil.AddFieldManagerFlagVar(cmd, &options.CreateSubcommandOptions.FieldManager, "kubectl-create") + cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") + cmdutil.AddDryRunFlag(cmd) return cmd } -// Complete completes all the required options -func (o *ServiceExternalNameOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { - name, err := NameFromCommandArgs(cmd, args) +func parsePorts(portString string) (int32, intstr.IntOrString, error) { + portStringSlice := strings.Split(portString, ":") + + port, err := utilsnet.ParsePort(portStringSlice[0], true) if err != nil { - return err + return 0, intstr.FromInt(0), err } - var generator generate.StructuredGenerator - switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { - case generateversioned.ServiceExternalNameGeneratorV1Name: - generator = &generateversioned.ServiceCommonGeneratorV1{ - Name: name, - Type: v1.ServiceTypeExternalName, - ExternalName: cmdutil.GetFlagString(cmd, "external-name"), - ClusterIP: "", + if len(portStringSlice) == 1 { + return int32(port), intstr.FromInt(int(port)), nil + } + + var targetPort intstr.IntOrString + if portNum, err := strconv.Atoi(portStringSlice[1]); err != nil { + if errs := validation.IsValidPortName(portStringSlice[1]); len(errs) != 0 { + return 0, intstr.FromInt(0), fmt.Errorf(strings.Join(errs, ",")) } - default: - return errUnsupportedGenerator(cmd, generatorName) + targetPort = intstr.FromString(portStringSlice[1]) + } else { + if errs := validation.IsValidPortNum(portNum); len(errs) != 0 { + return 0, intstr.FromInt(0), fmt.Errorf(strings.Join(errs, ",")) + } + targetPort = intstr.FromInt(portNum) } - - return o.CreateSubcommandOptions.Complete(f, cmd, args, generator) -} - -// Run calls the CreateSubcommandOptions.Run in ServiceExternalNameOpts instance -func (o *ServiceExternalNameOpts) Run() error { - return o.CreateSubcommandOptions.Run() + return int32(port), targetPort, nil } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service_test.go index c8ef54c59db..8365a116765 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_service_test.go @@ -17,112 +17,253 @@ limitations under the License. package create import ( - "net/http" "testing" - "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/rest/fake" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - "k8s.io/kubectl/pkg/scheme" + v1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" ) -func TestCreateService(t *testing.T) { - service := &v1.Service{} - service.Name = "my-service" - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() +func TestCreateServices(t *testing.T) { - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - negSer := scheme.Codecs - - tf.Client = &fake.RESTClient{ - GroupVersion: schema.GroupVersion{Version: "v1"}, - NegotiatedSerializer: negSer, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/services" && m == "POST": - return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, service)}, nil - default: - t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) - return nil, nil - } - }), + tests := []struct { + name string + serviceType v1.ServiceType + tcp []string + clusterip string + externalName string + nodeport int + expected *v1.Service + expectErr bool + }{ + { + name: "clusterip-ok", + tcp: []string{"456", "321:908"}, + clusterip: "", + serviceType: v1.ServiceTypeClusterIP, + expected: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterip-ok", + Labels: map[string]string{"app": "clusterip-ok"}, + }, + Spec: v1.ServiceSpec{Type: "ClusterIP", + Ports: []v1.ServicePort{{Name: "456", Protocol: "TCP", Port: 456, TargetPort: intstr.IntOrString{Type: 0, IntVal: 456, StrVal: ""}, NodePort: 0}, + {Name: "321-908", Protocol: "TCP", Port: 321, TargetPort: intstr.IntOrString{Type: 0, IntVal: 908, StrVal: ""}, NodePort: 0}}, + Selector: map[string]string{"app": "clusterip-ok"}, + ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: ""}, + }, + expectErr: false, + }, + { + name: "clusterip-missing", + serviceType: v1.ServiceTypeClusterIP, + expectErr: true, + }, + { + name: "clusterip-none-wrong-type", + tcp: []string{}, + clusterip: "None", + serviceType: v1.ServiceTypeNodePort, + expectErr: true, + }, + { + name: "clusterip-none-ok", + tcp: []string{}, + clusterip: "None", + serviceType: v1.ServiceTypeClusterIP, + expected: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterip-none-ok", + Labels: map[string]string{"app": "clusterip-none-ok"}, + }, + Spec: v1.ServiceSpec{Type: "ClusterIP", + Ports: []v1.ServicePort{}, + Selector: map[string]string{"app": "clusterip-none-ok"}, + ClusterIP: "None", ExternalIPs: []string(nil), LoadBalancerIP: ""}, + }, + expectErr: false, + }, + { + name: "clusterip-none-and-port-mapping", + tcp: []string{"456:9898"}, + clusterip: "None", + serviceType: v1.ServiceTypeClusterIP, + expected: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clusterip-none-and-port-mapping", + Labels: map[string]string{"app": "clusterip-none-and-port-mapping"}, + }, + Spec: v1.ServiceSpec{Type: "ClusterIP", + Ports: []v1.ServicePort{{Name: "456-9898", Protocol: "TCP", Port: 456, TargetPort: intstr.IntOrString{Type: 0, IntVal: 9898, StrVal: ""}, NodePort: 0}}, + Selector: map[string]string{"app": "clusterip-none-and-port-mapping"}, + ClusterIP: "None", ExternalIPs: []string(nil), LoadBalancerIP: ""}, + }, + expectErr: false, + }, + { + name: "loadbalancer-ok", + tcp: []string{"456:9898"}, + clusterip: "", + serviceType: v1.ServiceTypeLoadBalancer, + expected: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "loadbalancer-ok", + Labels: map[string]string{"app": "loadbalancer-ok"}, + }, + Spec: v1.ServiceSpec{Type: "LoadBalancer", + Ports: []v1.ServicePort{{Name: "456-9898", Protocol: "TCP", Port: 456, TargetPort: intstr.IntOrString{Type: 0, IntVal: 9898, StrVal: ""}, NodePort: 0}}, + Selector: map[string]string{"app": "loadbalancer-ok"}, + ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: ""}, + }, + expectErr: false, + }, + { + name: "invalid-port", + tcp: []string{"65536"}, + clusterip: "None", + serviceType: v1.ServiceTypeClusterIP, + expectErr: true, + }, + { + name: "invalid-port-mapping", + tcp: []string{"8080:-abc"}, + clusterip: "None", + serviceType: v1.ServiceTypeClusterIP, + expectErr: true, + }, + { + expectErr: true, + }, + { + name: "validate-ok", + serviceType: v1.ServiceTypeClusterIP, + tcp: []string{"123", "234:1234"}, + clusterip: "", + expected: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "validate-ok", + Labels: map[string]string{"app": "validate-ok"}, + }, + Spec: v1.ServiceSpec{Type: "ClusterIP", + Ports: []v1.ServicePort{ + {Name: "123", Protocol: "TCP", Port: 123, TargetPort: intstr.IntOrString{Type: 0, IntVal: 123, StrVal: ""}, NodePort: 0}, + {Name: "234-1234", Protocol: "TCP", Port: 234, TargetPort: intstr.IntOrString{Type: 0, IntVal: 1234, StrVal: ""}, NodePort: 0}, + }, + Selector: map[string]string{"app": "validate-ok"}, + ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: ""}, + }, + expectErr: false, + }, + { + name: "invalid-ClusterIPNone", + serviceType: v1.ServiceTypeNodePort, + tcp: []string{"123", "234:1234"}, + clusterip: v1.ClusterIPNone, + expectErr: true, + }, + { + name: "TCP-none", + serviceType: v1.ServiceTypeClusterIP, + clusterip: "", + expectErr: true, + }, + { + name: "invalid-ExternalName", + serviceType: v1.ServiceTypeExternalName, + tcp: []string{"123", "234:1234"}, + clusterip: "", + externalName: "@oi:test", + expectErr: true, + }, + { + name: "externalName-ok", + serviceType: v1.ServiceTypeExternalName, + tcp: []string{"123", "234:1234"}, + clusterip: "", + externalName: "www.externalname.com", + expected: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "externalName-ok", + Labels: map[string]string{"app": "externalName-ok"}, + }, + Spec: v1.ServiceSpec{Type: "ExternalName", + Ports: []v1.ServicePort{ + {Name: "123", Protocol: "TCP", Port: 123, TargetPort: intstr.IntOrString{Type: 0, IntVal: 123, StrVal: ""}, NodePort: 0}, + {Name: "234-1234", Protocol: "TCP", Port: 234, TargetPort: intstr.IntOrString{Type: 0, IntVal: 1234, StrVal: ""}, NodePort: 0}, + }, + Selector: map[string]string{"app": "externalName-ok"}, + ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: "", ExternalName: "www.externalname.com"}, + }, + expectErr: false, + }, + { + name: "my-node-port-service-ok", + serviceType: v1.ServiceTypeNodePort, + tcp: []string{"443:https", "30000:8000"}, + clusterip: "", + expected: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node-port-service-ok", + Labels: map[string]string{"app": "my-node-port-service-ok"}, + }, + Spec: v1.ServiceSpec{Type: "NodePort", + Ports: []v1.ServicePort{ + {Name: "443-https", Protocol: "TCP", Port: 443, TargetPort: intstr.IntOrString{Type: 1, IntVal: 0, StrVal: "https"}, NodePort: 0}, + {Name: "30000-8000", Protocol: "TCP", Port: 30000, TargetPort: intstr.IntOrString{Type: 0, IntVal: 8000, StrVal: ""}, NodePort: 0}, + }, + Selector: map[string]string{"app": "my-node-port-service-ok"}, + ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: ""}, + }, + expectErr: false, + }, + { + name: "my-node-port-service-ok2", + serviceType: v1.ServiceTypeNodePort, + tcp: []string{"80:http"}, + clusterip: "", + nodeport: 4444, + expected: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node-port-service-ok2", + Labels: map[string]string{"app": "my-node-port-service-ok2"}, + }, + Spec: v1.ServiceSpec{Type: "NodePort", + Ports: []v1.ServicePort{ + {Name: "80-http", Protocol: "TCP", Port: 80, TargetPort: intstr.IntOrString{Type: 1, IntVal: 0, StrVal: "http"}, NodePort: 4444}, + }, + Selector: map[string]string{"app": "my-node-port-service-ok2"}, + ClusterIP: "", ExternalIPs: []string(nil), LoadBalancerIP: ""}, + }, + expectErr: false, + }, } - ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams() - cmd := NewCmdCreateServiceClusterIP(tf, ioStreams) - cmd.Flags().Set("output", "name") - cmd.Flags().Set("tcp", "8080:8000") - cmd.Run(cmd, []string{service.Name}) - expectedOutput := "service/" + service.Name + "\n" - if buf.String() != expectedOutput { - t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) - } -} - -func TestCreateServiceNodePort(t *testing.T) { - service := &v1.Service{} - service.Name = "my-node-port-service" - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() - - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - negSer := scheme.Codecs - - tf.Client = &fake.RESTClient{ - GroupVersion: schema.GroupVersion{Version: "v1"}, - NegotiatedSerializer: negSer, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/services" && m == http.MethodPost: - return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, service)}, nil - default: - t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) - return nil, nil - } - }), - } - ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams() - cmd := NewCmdCreateServiceNodePort(tf, ioStreams) - cmd.Flags().Set("output", "name") - cmd.Flags().Set("tcp", "30000:8000") - cmd.Run(cmd, []string{service.Name}) - expectedOutput := "service/" + service.Name + "\n" - if buf.String() != expectedOutput { - t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) - } -} - -func TestCreateServiceExternalName(t *testing.T) { - service := &v1.Service{} - service.Name = "my-external-name-service" - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() - - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - negSer := scheme.Codecs - - tf.Client = &fake.RESTClient{ - GroupVersion: schema.GroupVersion{Version: "v1"}, - NegotiatedSerializer: negSer, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/services" && m == http.MethodPost: - return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, service)}, nil - default: - t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) - return nil, nil - } - }), - } - ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams() - cmd := NewCmdCreateServiceExternalName(tf, ioStreams) - cmd.Flags().Set("output", "name") - cmd.Flags().Set("external-name", "name") - cmd.Run(cmd, []string{service.Name}) - expectedOutput := "service/" + service.Name + "\n" - if buf.String() != expectedOutput { - t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + options := ServiceOptions{ + Name: tc.name, + Type: tc.serviceType, + TCP: tc.tcp, + ClusterIP: tc.clusterip, + NodePort: tc.nodeport, + ExternalName: tc.externalName, + } + + var service *v1.Service + + err := options.Validate() + if err == nil { + service, err = options.createService() + } + if tc.expectErr && err == nil { + t.Errorf("%s: expected an error, but createService passes.", tc.name) + } + if !tc.expectErr && err != nil { + t.Errorf("%s: unexpected error: %v", tc.name, err) + } + if !apiequality.Semantic.DeepEqual(service, tc.expected) { + t.Errorf("%s: expected:\n%#v\ngot:\n%#v", tc.name, tc.expected, service) + } + }) } } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_serviceaccount.go b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_serviceaccount.go index 20cb73dc81e..2c45cb11c96 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_serviceaccount.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_serviceaccount.go @@ -91,3 +91,7 @@ func (o *ServiceAccountOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, arg func (o *ServiceAccountOpts) Run() error { return o.CreateSubcommandOptions.Run() } + +func errUnsupportedGenerator(cmd *cobra.Command, generatorName string) error { + return cmdutil.UsageErrorf(cmd, "Generator %s not supported. ", generatorName) +}