From de77e6a05bd4d7321bf40397de41f3a4af5bf9a8 Mon Sep 17 00:00:00 2001 From: Chok Yip Lau Date: Sat, 4 Dec 2021 16:38:01 -0500 Subject: [PATCH] Remove generator dependency of expose.go Kubernetes-commit: d7ab8d442b7913e5eb69fc2ce93b2b704645b089 --- pkg/cmd/expose/expose.go | 314 +++++++++--- pkg/cmd/expose/expose_test.go | 877 ++++++++++++++++++++++++++++++++++ 2 files changed, 1129 insertions(+), 62 deletions(-) diff --git a/pkg/cmd/expose/expose.go b/pkg/cmd/expose/expose.go index c69b72bbd..8862086ae 100644 --- a/pkg/cmd/expose/expose.go +++ b/pkg/cmd/expose/expose.go @@ -17,7 +17,9 @@ limitations under the License. package expose import ( + "fmt" "regexp" + "strconv" "strings" "github.com/spf13/cobra" @@ -25,16 +27,17 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme" "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/printers" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/generate" - generateversioned "k8s.io/kubectl/pkg/generate/versioned" "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" @@ -83,6 +86,7 @@ var ( kubectl expose deployment nginx --port=80 --target-port=8000`)) ) +// ExposeServiceOptions holds the options for kubectl expose command type ExposeServiceOptions struct { cmdutil.OverrideOptions @@ -91,13 +95,31 @@ type ExposeServiceOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj printers.ResourcePrinterFunc + Name string + DefaultName string + Selector string + // Port will be used if a user specifies --port OR the exposed object as one port + Port string + // Ports will be used iff a user doesn't specify --port AND the exposed object has multiple ports + Ports string + Labels string + ExternalIP string + LoadBalancerIP string + Type string + Protocol string + // Protocols will be used to keep port-protocol mapping derived from exposed object + Protocols string + TargetPort string + PortName string + SessionAffinity string + ClusterIP string + DryRunStrategy cmdutil.DryRunStrategy DryRunVerifier *resource.QueryParamVerifier EnforceNamespace bool fieldManager string - Generators func(string) map[string]generate.Generator CanBeExposed polymorphichelpers.CanBeExposedFunc MapBasedSelectorForObject func(runtime.Object) (string, error) PortsForObject polymorphichelpers.PortsForObjectFunc @@ -113,6 +135,8 @@ type ExposeServiceOptions struct { genericclioptions.IOStreams } +// NewExposeServiceOptions creates a new ExposeServiceOptions and return a pointer to the +// struct func NewExposeServiceOptions(ioStreams genericclioptions.IOStreams) *ExposeServiceOptions { return &ExposeServiceOptions{ RecordFlags: genericclioptions.NewRecordFlags(), @@ -123,6 +147,7 @@ func NewExposeServiceOptions(ioStreams genericclioptions.IOStreams) *ExposeServi } } +// NewCmdExposeService is a command to expose the service from user's input func NewCmdExposeService(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { o := NewExposeServiceOptions(streams) @@ -148,20 +173,17 @@ func NewCmdExposeService(f cmdutil.Factory, streams genericclioptions.IOStreams) o.RecordFlags.AddFlags(cmd) o.PrintFlags.AddFlags(cmd) - cmd.Flags().String("generator", "service/v2", i18n.T("The name of the API generator to use. There are 2 generators: 'service/v1' and 'service/v2'. The only difference between them is that service port in v1 is named 'default', while it is left unnamed in v2. Default is 'service/v2'.")) - cmd.Flags().String("protocol", "", i18n.T("The network protocol for the service to be created. Default is 'TCP'.")) - cmd.Flags().String("port", "", i18n.T("The port that the service should serve on. Copied from the resource being exposed, if unspecified")) - cmd.Flags().String("type", "", i18n.T("Type for this service: ClusterIP, NodePort, LoadBalancer, or ExternalName. Default is 'ClusterIP'.")) - cmd.Flags().String("load-balancer-ip", "", i18n.T("IP to assign to the LoadBalancer. If empty, an ephemeral IP will be created and used (cloud-provider specific).")) - cmd.Flags().String("selector", "", i18n.T("A label selector to use for this service. Only equality-based selector requirements are supported. If empty (the default) infer the selector from the replication controller or replica set.)")) - cmd.Flags().StringP("labels", "l", "", "Labels to apply to the service created by this call.") - cmd.Flags().String("container-port", "", i18n.T("Synonym for --target-port")) - cmd.Flags().MarkDeprecated("container-port", "--container-port will be removed in the future, please use --target-port instead") - cmd.Flags().String("target-port", "", i18n.T("Name or number for the port on the container that the service should direct traffic to. Optional.")) - cmd.Flags().String("external-ip", "", i18n.T("Additional external IP address (not managed by Kubernetes) to accept for the service. If this IP is routed to a node, the service can be accessed by this IP in addition to its generated service IP.")) - cmd.Flags().String("name", "", i18n.T("The name for the newly created object.")) - cmd.Flags().String("session-affinity", "", i18n.T("If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP'")) - cmd.Flags().String("cluster-ip", "", i18n.T("ClusterIP to be assigned to the service. Leave empty to auto-allocate, or set to 'None' to create a headless service.")) + cmd.Flags().StringVar(&o.Protocol, "protocol", o.Protocol, i18n.T("The network protocol for the service to be created. Default is 'TCP'.")) + cmd.Flags().StringVar(&o.Port, "port", o.Port, i18n.T("The port that the service should serve on. Copied from the resource being exposed, if unspecified")) + cmd.Flags().StringVar(&o.Type, "type", o.Type, i18n.T("Type for this service: ClusterIP, NodePort, LoadBalancer, or ExternalName. Default is 'ClusterIP'.")) + cmd.Flags().StringVar(&o.LoadBalancerIP, "load-balancer-ip", o.LoadBalancerIP, i18n.T("IP to assign to the LoadBalancer. If empty, an ephemeral IP will be created and used (cloud-provider specific).")) + cmd.Flags().StringVar(&o.Selector, "selector", o.Selector, i18n.T("A label selector to use for this service. Only equality-based selector requirements are supported. If empty (the default) infer the selector from the replication controller or replica set.)")) + cmd.Flags().StringVarP(&o.Labels, "labels", "l", o.Labels, "Labels to apply to the service created by this call.") + cmd.Flags().StringVar(&o.TargetPort, "target-port", o.TargetPort, i18n.T("Name or number for the port on the container that the service should direct traffic to. Optional.")) + cmd.Flags().StringVar(&o.ExternalIP, "external-ip", o.ExternalIP, i18n.T("Additional external IP address (not managed by Kubernetes) to accept for the service. If this IP is routed to a node, the service can be accessed by this IP in addition to its generated service IP.")) + cmd.Flags().StringVar(&o.Name, "name", o.Name, i18n.T("The name for the newly created object.")) + cmd.Flags().StringVar(&o.SessionAffinity, "session-affinity", o.SessionAffinity, i18n.T("If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP'")) + cmd.Flags().StringVar(&o.ClusterIP, "cluster-ip", o.ClusterIP, i18n.T("ClusterIP to be assigned to the service. Leave empty to auto-allocate, or set to 'None' to create a headless service.")) cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-expose") o.AddOverrideFlags(cmd) @@ -172,6 +194,7 @@ func NewCmdExposeService(f cmdutil.Factory, streams genericclioptions.IOStreams) return cmd } +// Complete loads data from the command line environment func (o *ExposeServiceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { var err error o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) @@ -197,7 +220,6 @@ func (o *ExposeServiceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) e return err } - o.Generators = generateversioned.GeneratorFn o.Builder = f.NewBuilder() o.ClientForMapping = f.ClientForMapping o.CanBeExposed = polymorphichelpers.CanBeExposedFn @@ -218,6 +240,8 @@ func (o *ExposeServiceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) e return err } +// RunExpose retrieves the Kubernetes Object from the API server and expose it to a +// Kubernetes Service func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) error { r := o.Builder. WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). @@ -229,17 +253,8 @@ func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) erro Do() err := r.Err() if err != nil { - return cmdutil.UsageErrorf(cmd, err.Error()) - } - - // Get the generator, setup and validate all required parameters - generatorName := cmdutil.GetFlagString(cmd, "generator") - generators := o.Generators("expose") - generator, found := generators[generatorName] - if !found { - return cmdutil.UsageErrorf(cmd, "generator %q not found.", generatorName) + return err } - names := generator.ParamNames() err = r.Visit(func(info *resource.Info, err error) error { if err != nil { @@ -251,98 +266,90 @@ func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) erro return err } - params := generate.MakeParams(cmd, names) name := info.Name if len(name) > validation.DNS1035LabelMaxLength { name = name[:validation.DNS1035LabelMaxLength] } - params["default-name"] = name + o.DefaultName = name // For objects that need a pod selector, derive it from the exposed object in case a user // didn't explicitly specify one via --selector - if s, found := params["selector"]; found && generate.IsZero(s) { + if len(o.Selector) == 0 { s, err := o.MapBasedSelectorForObject(info.Object) if err != nil { - return cmdutil.UsageErrorf(cmd, "couldn't retrieve selectors via --selector flag or introspection: %v", err) + return fmt.Errorf("couldn't retrieve selectors via --selector flag or introspection: %v", err) } - params["selector"] = s + o.Selector = s } - isHeadlessService := params["cluster-ip"] == "None" + isHeadlessService := o.ClusterIP == "None" // For objects that need a port, derive it from the exposed object in case a user // didn't explicitly specify one via --port - if port, found := params["port"]; found && generate.IsZero(port) { + if len(o.Port) == 0 { ports, err := o.PortsForObject(info.Object) if err != nil { - return cmdutil.UsageErrorf(cmd, "couldn't find port via --port flag or introspection: %v", err) + return fmt.Errorf("couldn't find port via --port flag or introspection: %v", err) } switch len(ports) { case 0: if !isHeadlessService { - return cmdutil.UsageErrorf(cmd, "couldn't find port via --port flag or introspection") + return fmt.Errorf("couldn't find port via --port flag or introspection") } case 1: - params["port"] = ports[0] + o.Port = ports[0] default: - params["ports"] = strings.Join(ports, ",") + o.Ports = strings.Join(ports, ",") } } // Always try to derive protocols from the exposed object, may use // different protocols for different ports. - if _, found := params["protocol"]; found { - protocolsMap, err := o.ProtocolsForObject(info.Object) - if err != nil { - return cmdutil.UsageErrorf(cmd, "couldn't find protocol via introspection: %v", err) - } - if protocols := generate.MakeProtocols(protocolsMap); !generate.IsZero(protocols) { - params["protocols"] = protocols - } + protocolsMap, err := o.ProtocolsForObject(info.Object) + if err != nil { + return fmt.Errorf("couldn't find protocol via introspection: %v", err) + } + if protocols := generate.MakeProtocols(protocolsMap); !generate.IsZero(protocols) { + o.Protocols = protocols } - if generate.IsZero(params["labels"]) { + if len(o.Labels) == 0 { labels, err := meta.NewAccessor().Labels(info.Object) if err != nil { return err } - params["labels"] = polymorphichelpers.MakeLabels(labels) - } - if err = generate.ValidateParams(names, params); err != nil { - return err - } - // Check for invalid flags used against the present generator. - if err := generate.EnsureFlagsValid(cmd, generators, generatorName); err != nil { - return err + o.Labels = polymorphichelpers.MakeLabels(labels) } // Generate new object - object, err := generator.Generate(params) + service, err := o.createService() + if err != nil { return err } - object, err = o.NewOverrider(&corev1.Service{}).Apply(object) + overrideService, err := o.NewOverrider(&corev1.Service{}).Apply(service) + if err != nil { return err } - if err := o.Recorder.Record(object); err != nil { + if err := o.Recorder.Record(overrideService); err != nil { klog.V(4).Infof("error recording current command: %v", err) } if o.DryRunStrategy == cmdutil.DryRunClient { - if meta, err := meta.Accessor(object); err == nil && o.EnforceNamespace { + if meta, err := meta.Accessor(overrideService); err == nil && o.EnforceNamespace { meta.SetNamespace(o.Namespace) } - return o.PrintObj(object, o.Out) + return o.PrintObj(overrideService, o.Out) } - if err := util.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), object, scheme.DefaultJSONEncoder()); err != nil { + if err := util.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), overrideService, scheme.DefaultJSONEncoder()); err != nil { return err } asUnstructured := &unstructured.Unstructured{} - if err := scheme.Scheme.Convert(object, asUnstructured, nil); err != nil { + if err := scheme.Scheme.Convert(overrideService, asUnstructured, nil); err != nil { return err } gvks, _, err := unstructuredscheme.NewUnstructuredObjectTyper().ObjectKinds(asUnstructured) @@ -379,3 +386,186 @@ func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) erro } return nil } + +func (o *ExposeServiceOptions) createService() (*corev1.Service, error) { + if len(o.Selector) == 0 { + return nil, fmt.Errorf("selector must be specified") + } + selector, err := parseLabels(o.Selector) + if err != nil { + return nil, err + } + + var labels map[string]string + if len(o.Labels) > 0 { + labels, err = parseLabels(o.Labels) + if err != nil { + return nil, err + } + } + + name := o.Name + if len(name) == 0 { + name = o.DefaultName + if len(name) == 0 { + return nil, fmt.Errorf("name must be specified") + } + } + + var portProtocolMap map[string]string + if o.Protocols != "" { + portProtocolMap, err = parseProtocols(o.Protocols) + if err != nil { + return nil, err + } + } + + // ports takes precedence over port since it will be + // specified only when the user hasn't specified a port + // via --port and the exposed object has multiple ports. + var portString string + portString = o.Ports + if len(o.Ports) == 0 { + portString = o.Port + } + + ports := []corev1.ServicePort{} + if len(portString) != 0 { + portStringSlice := strings.Split(portString, ",") + servicePortName := o.PortName + for i, stillPortString := range portStringSlice { + port, err := strconv.Atoi(stillPortString) + if err != nil { + return nil, err + } + name := servicePortName + // If we are going to assign multiple ports to a service, we need to + // generate a different name for each one. + if len(portStringSlice) > 1 { + name = fmt.Sprintf("port-%d", i+1) + } + protocol := o.Protocol + + switch { + case len(protocol) == 0 && len(portProtocolMap) == 0: + // Default to TCP, what the flag was doing previously. + protocol = "TCP" + case len(protocol) > 0 && len(portProtocolMap) > 0: + // User has specified the --protocol while exposing a multiprotocol resource + // We should stomp multiple protocols with the one specified ie. do nothing + case len(protocol) == 0 && len(portProtocolMap) > 0: + // no --protocol and we expose a multiprotocol resource + protocol = "TCP" // have the default so we can stay sane + if exposeProtocol, found := portProtocolMap[stillPortString]; found { + protocol = exposeProtocol + } + } + ports = append(ports, corev1.ServicePort{ + Name: name, + Port: int32(port), + Protocol: corev1.Protocol(protocol), + }) + } + } + + service := corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + Selector: selector, + Ports: ports, + }, + } + targetPortString := o.TargetPort + if len(targetPortString) > 0 { + var targetPort intstr.IntOrString + if portNum, err := strconv.Atoi(targetPortString); err != nil { + targetPort = intstr.FromString(targetPortString) + } else { + targetPort = intstr.FromInt(portNum) + } + // Use the same target-port for every port + for i := range service.Spec.Ports { + service.Spec.Ports[i].TargetPort = targetPort + } + } else { + // If --target-port or --container-port haven't been specified, this + // should be the same as Port + for i := range service.Spec.Ports { + port := service.Spec.Ports[i].Port + service.Spec.Ports[i].TargetPort = intstr.FromInt(int(port)) + } + } + if len(o.ExternalIP) > 0 { + service.Spec.ExternalIPs = []string{o.ExternalIP} + } + if len(o.Type) != 0 { + service.Spec.Type = corev1.ServiceType(o.Type) + } + if service.Spec.Type == corev1.ServiceTypeLoadBalancer { + service.Spec.LoadBalancerIP = o.LoadBalancerIP + } + if len(o.SessionAffinity) != 0 { + switch corev1.ServiceAffinity(o.SessionAffinity) { + case corev1.ServiceAffinityNone: + service.Spec.SessionAffinity = corev1.ServiceAffinityNone + case corev1.ServiceAffinityClientIP: + service.Spec.SessionAffinity = corev1.ServiceAffinityClientIP + default: + return nil, fmt.Errorf("unknown session affinity: %s", o.SessionAffinity) + } + } + if len(o.ClusterIP) != 0 { + if o.ClusterIP == "None" { + service.Spec.ClusterIP = corev1.ClusterIPNone + } else { + service.Spec.ClusterIP = o.ClusterIP + } + } + return &service, nil +} + +// parseLabels turns a string representation of a label set into a map[string]string +func parseLabels(labelSpec string) (map[string]string, error) { + if len(labelSpec) == 0 { + return nil, fmt.Errorf("no label spec passed") + } + labels := map[string]string{} + labelSpecs := strings.Split(labelSpec, ",") + for ix := range labelSpecs { + labelSpec := strings.Split(labelSpecs[ix], "=") + if len(labelSpec) != 2 { + return nil, fmt.Errorf("unexpected label spec: %s", labelSpecs[ix]) + } + if len(labelSpec[0]) == 0 { + return nil, fmt.Errorf("unexpected empty label key") + } + labels[labelSpec[0]] = labelSpec[1] + } + return labels, nil +} + +// parseProtocols turns a string representation of a protocols set into a map[string]string +func parseProtocols(protocols string) (map[string]string, error) { + if len(protocols) == 0 { + return nil, fmt.Errorf("no protocols passed") + } + portProtocolMap := map[string]string{} + protocolsSlice := strings.Split(protocols, ",") + for ix := range protocolsSlice { + portProtocol := strings.Split(protocolsSlice[ix], "/") + if len(portProtocol) != 2 { + return nil, fmt.Errorf("unexpected port protocol mapping: %s", protocolsSlice[ix]) + } + if len(portProtocol[0]) == 0 { + return nil, fmt.Errorf("unexpected empty port") + } + if len(portProtocol[1]) == 0 { + return nil, fmt.Errorf("unexpected empty protocol") + } + portProtocolMap[portProtocol[0]] = portProtocol[1] + } + return portProtocolMap, nil +} diff --git a/pkg/cmd/expose/expose_test.go b/pkg/cmd/expose/expose_test.go index 18dc26c90..0f1f9246a 100644 --- a/pkg/cmd/expose/expose_test.go +++ b/pkg/cmd/expose/expose_test.go @@ -21,7 +21,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -856,3 +858,878 @@ status: }) } } + +func TestGenerateService(t *testing.T) { + tests := map[string]struct { + selector string + name string + port string + protocol string + protocols string + targetPort string + clusterIP string + labels string + externalIP string + serviceType string + sessionAffinity string + setup func(t *testing.T, exposeServiceOptions *ExposeServiceOptions) func() + + expected *corev1.Service + expectErr string + }{ + "test1": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "TCP", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "TCP", + TargetPort: intstr.FromInt(1234), + }, + }, + }, + }, + }, + "test2": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "UDP", + targetPort: "foobar", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "UDP", + TargetPort: intstr.FromString("foobar"), + }, + }, + }, + }, + }, + "test3": { + selector: "foo=bar,baz=blah", + labels: "key1=value1,key2=value2", + name: "test", + port: "80", + protocol: "TCP", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "TCP", + TargetPort: intstr.FromInt(1234), + }, + }, + }, + }, + }, + "test4": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "UDP", + externalIP: "1.2.3.4", + targetPort: "foobar", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "UDP", + TargetPort: intstr.FromString("foobar"), + }, + }, + ExternalIPs: []string{"1.2.3.4"}, + }, + }, + }, + "test5": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "UDP", + externalIP: "1.2.3.4", + serviceType: "LoadBalancer", + targetPort: "foobar", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "UDP", + TargetPort: intstr.FromString("foobar"), + }, + }, + Type: corev1.ServiceTypeLoadBalancer, + ExternalIPs: []string{"1.2.3.4"}, + }, + }, + }, + "test6": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "UDP", + targetPort: "foobar", + serviceType: string(corev1.ServiceTypeNodePort), + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "UDP", + TargetPort: intstr.FromString("foobar"), + }, + }, + Type: corev1.ServiceTypeNodePort, + }, + }, + }, + "test7": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "UDP", + targetPort: "foobar", + serviceType: string(corev1.ServiceTypeNodePort), + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "UDP", + TargetPort: intstr.FromString("foobar"), + }, + }, + Type: corev1.ServiceTypeNodePort, + }, + }, + }, + "test8": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "TCP", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "TCP", + TargetPort: intstr.FromInt(1234), + }, + }, + }, + }, + }, + "test9": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "TCP", + sessionAffinity: "ClientIP", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "TCP", + TargetPort: intstr.FromInt(1234), + }, + }, + SessionAffinity: corev1.ServiceAffinityClientIP, + }, + }, + }, + "test10": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "TCP", + clusterIP: "10.10.10.10", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + + Port: 80, + Protocol: "TCP", + TargetPort: intstr.FromInt(1234), + }, + }, + ClusterIP: "10.10.10.10", + }, + }, + }, + "test11": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "TCP", + clusterIP: "None", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + + Port: 80, + Protocol: "TCP", + TargetPort: intstr.FromInt(1234), + }, + }, + ClusterIP: corev1.ClusterIPNone, + }, + }, + }, + "test12": { + selector: "foo=bar", + name: "test", + port: "80,443", + protocol: "TCP", + targetPort: "foobar", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Ports: []corev1.ServicePort{ + { + Name: "port-1", + Port: 80, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromString("foobar"), + }, + { + Name: "port-2", + Port: 443, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromString("foobar"), + }, + }, + }, + }, + }, + "test13": { + selector: "foo=bar", + name: "test", + port: "80,443", + protocol: "UDP", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Ports: []corev1.ServicePort{ + { + Name: "port-1", + Port: 80, + Protocol: corev1.ProtocolUDP, + TargetPort: intstr.FromInt(1234), + }, + { + Name: "port-2", + Port: 443, + Protocol: corev1.ProtocolUDP, + TargetPort: intstr.FromInt(1234), + }, + }, + }, + }, + }, + "test14": { + selector: "foo=bar", + name: "test", + port: "80,443", + protocol: "TCP", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Ports: []corev1.ServicePort{ + { + Name: "port-1", + Port: 80, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(80), + }, + { + Name: "port-2", + Port: 443, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(443), + }, + }, + }, + }, + }, + "test15": { + selector: "foo=bar", + name: "test", + port: "80,8080", + protocols: "8080/UDP", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Ports: []corev1.ServicePort{ + { + Name: "port-1", + Port: 80, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(80), + }, + { + Name: "port-2", + Port: 8080, + Protocol: corev1.ProtocolUDP, + TargetPort: intstr.FromInt(8080), + }, + }, + }, + }, + }, + "test16": { + selector: "foo=bar", + name: "test", + port: "80,8080,8081", + protocols: "8080/UDP,8081/TCP", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Ports: []corev1.ServicePort{ + { + Name: "port-1", + Port: 80, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(80), + }, + { + Name: "port-2", + Port: 8080, + Protocol: corev1.ProtocolUDP, + TargetPort: intstr.FromInt(8080), + }, + { + Name: "port-3", + Port: 8081, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(8081), + }, + }, + }, + }, + }, + "test17": { + selector: "foo=bar,baz=blah", + name: "test", + protocol: "TCP", + clusterIP: "None", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{}, + ClusterIP: corev1.ClusterIPNone, + }, + }, + }, + "test18": { + selector: "foo=bar", + name: "test", + clusterIP: "None", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Ports: []corev1.ServicePort{}, + ClusterIP: corev1.ClusterIPNone, + }, + }, + }, + "test19": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "SCTP", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + + Port: 80, + Protocol: "SCTP", + TargetPort: intstr.FromInt(1234), + }, + }, + }, + }, + }, + "test20": { + selector: "foo=bar,baz=blah", + labels: "key1=value1,key2=value2", + name: "test", + port: "80", + protocol: "SCTP", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Labels: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + + Port: 80, + Protocol: "SCTP", + TargetPort: intstr.FromInt(1234), + }, + }, + }, + }, + }, + "test21": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "SCTP", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + + Port: 80, + Protocol: "SCTP", + TargetPort: intstr.FromInt(1234), + }, + }, + }, + }, + }, + "test22": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "SCTP", + sessionAffinity: "ClientIP", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "SCTP", + TargetPort: intstr.FromInt(1234), + }, + }, + SessionAffinity: corev1.ServiceAffinityClientIP, + }, + }, + }, + "test23": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "SCTP", + clusterIP: "10.10.10.10", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + Protocol: "SCTP", + TargetPort: intstr.FromInt(1234), + }, + }, + ClusterIP: "10.10.10.10", + }, + }, + }, + "test24": { + selector: "foo=bar,baz=blah", + name: "test", + port: "80", + protocol: "SCTP", + clusterIP: "None", + targetPort: "1234", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{ + { + + Port: 80, + Protocol: "SCTP", + TargetPort: intstr.FromInt(1234), + }, + }, + ClusterIP: corev1.ClusterIPNone, + }, + }, + }, + "test25": { + selector: "foo=bar", + name: "test", + port: "80,443", + protocol: "SCTP", + targetPort: "foobar", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Ports: []corev1.ServicePort{ + { + Name: "port-1", + Port: 80, + Protocol: corev1.ProtocolSCTP, + TargetPort: intstr.FromString("foobar"), + }, + { + Name: "port-2", + Port: 443, + Protocol: corev1.ProtocolSCTP, + TargetPort: intstr.FromString("foobar"), + }, + }, + }, + }, + }, + "test26": { + selector: "foo=bar", + name: "test", + port: "80,443", + protocol: "SCTP", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Ports: []corev1.ServicePort{ + { + Name: "port-1", + Port: 80, + Protocol: corev1.ProtocolSCTP, + TargetPort: intstr.FromInt(80), + }, + { + Name: "port-2", + Port: 443, + Protocol: corev1.ProtocolSCTP, + TargetPort: intstr.FromInt(443), + }, + }, + }, + }, + }, + "test27": { + selector: "foo=bar", + name: "test", + port: "80,8080", + protocols: "8080/SCTP", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Ports: []corev1.ServicePort{ + { + Name: "port-1", + Port: 80, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(80), + }, + { + Name: "port-2", + Port: 8080, + Protocol: corev1.ProtocolSCTP, + TargetPort: intstr.FromInt(8080), + }, + }, + }, + }, + }, + "test28": { + selector: "foo=bar", + name: "test", + port: "80,8080,8081,8082", + protocols: "8080/UDP,8081/TCP,8082/SCTP", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Ports: []corev1.ServicePort{ + { + Name: "port-1", + Port: 80, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(80), + }, + { + Name: "port-2", + Port: 8080, + Protocol: corev1.ProtocolUDP, + TargetPort: intstr.FromInt(8080), + }, + { + Name: "port-3", + Port: 8081, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(8081), + }, + { + Name: "port-4", + Port: 8082, + Protocol: corev1.ProtocolSCTP, + TargetPort: intstr.FromInt(8082), + }, + }, + }, + }, + }, + "test 29": { + selector: "foo=bar,baz=blah", + name: "test", + protocol: "SCTP", + targetPort: "1234", + clusterIP: "None", + expected: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []corev1.ServicePort{}, + ClusterIP: corev1.ClusterIPNone, + }, + }, + }, + "check selector": { + name: "test", + protocol: "SCTP", + targetPort: "1234", + clusterIP: "None", + expectErr: `selector must be specified`, + }, + "check name": { + selector: "foo=bar,baz=blah", + protocol: "SCTP", + targetPort: "1234", + clusterIP: "None", + expectErr: `name must be specified`, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + exposeServiceOptions := ExposeServiceOptions{ + Selector: test.selector, + Name: test.name, + Protocol: test.protocol, + Protocols: test.protocols, + Port: test.port, + ClusterIP: test.clusterIP, + TargetPort: test.targetPort, + Labels: test.labels, + ExternalIP: test.externalIP, + Type: test.serviceType, + SessionAffinity: test.sessionAffinity, + } + + service, err := exposeServiceOptions.createService() + if test.expectErr == "" { + require.NoError(t, err) + if !apiequality.Semantic.DeepEqual(service, test.expected) { + t.Errorf("\nexpected:\n%#v\ngot:\n%#v", test.expected, service) + } + } else { + require.Error(t, err) + require.EqualError(t, err, test.expectErr) + } + + }) + + } +}