Merge pull request #106824 from lauchokyip/removeExposeGenerator

Remove generator dependency of expose.go

Kubernetes-commit: d429c9820e8570d093d1315e4b8f7a90af55d0a7
This commit is contained in:
Kubernetes Publisher 2022-03-29 17:34:07 -07:00
commit 573075ac9f
4 changed files with 1137 additions and 70 deletions

8
go.mod
View File

@ -31,10 +31,10 @@ require (
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 golang.org/x/sys v0.0.0-20220209214540-3681064d5158
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.0.0-20220329085248-f457c96d1ad5 k8s.io/api v0.0.0-20220330011017-6519413ffeb8
k8s.io/apimachinery v0.0.0-20220329130813-31e52c987dc1 k8s.io/apimachinery v0.0.0-20220329130813-31e52c987dc1
k8s.io/cli-runtime v0.0.0-20220329133126-053a01944821 k8s.io/cli-runtime v0.0.0-20220329133126-053a01944821
k8s.io/client-go v0.0.0-20220329091314-94a1081fafd8 k8s.io/client-go v0.0.0-20220329211335-0c14d0c48569
k8s.io/component-base v0.0.0-20220329051641-59f2f1feee51 k8s.io/component-base v0.0.0-20220329051641-59f2f1feee51
k8s.io/component-helpers v0.0.0-20220329051751-76fc5e36ae3a k8s.io/component-helpers v0.0.0-20220329051751-76fc5e36ae3a
k8s.io/klog/v2 v2.60.1 k8s.io/klog/v2 v2.60.1
@ -48,10 +48,10 @@ require (
) )
replace ( replace (
k8s.io/api => k8s.io/api v0.0.0-20220329085248-f457c96d1ad5 k8s.io/api => k8s.io/api v0.0.0-20220330011017-6519413ffeb8
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20220329130813-31e52c987dc1 k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20220329130813-31e52c987dc1
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20220329133126-053a01944821 k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20220329133126-053a01944821
k8s.io/client-go => k8s.io/client-go v0.0.0-20220329091314-94a1081fafd8 k8s.io/client-go => k8s.io/client-go v0.0.0-20220329211335-0c14d0c48569
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20220329130608-15a5f67a8af0 k8s.io/code-generator => k8s.io/code-generator v0.0.0-20220329130608-15a5f67a8af0
k8s.io/component-base => k8s.io/component-base v0.0.0-20220329051641-59f2f1feee51 k8s.io/component-base => k8s.io/component-base v0.0.0-20220329051641-59f2f1feee51
k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20220329051751-76fc5e36ae3a k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20220329051751-76fc5e36ae3a

8
go.sum
View File

@ -851,14 +851,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.0.0-20220329085248-f457c96d1ad5 h1:TXyh1FXkZDbvc5kgfNe3TIiOZWCmkrp/ZpvHdFEJxWc= k8s.io/api v0.0.0-20220330011017-6519413ffeb8 h1:0ie5+3B+m1N1rfQY2hVBFxd4cRS1mjD2teNijATNEuU=
k8s.io/api v0.0.0-20220329085248-f457c96d1ad5/go.mod h1:9dsqcUbHQ0TcKcDbIZizDq/CAc9ZzLsdgUQ0EE1Jqbo= k8s.io/api v0.0.0-20220330011017-6519413ffeb8/go.mod h1:DtY4NYpJTdEZbyZNYT6OchYvejyTwdm7DK07dMzPcII=
k8s.io/apimachinery v0.0.0-20220329130813-31e52c987dc1 h1:ESix+UawmXFu9oem7ngnHSbufLCiIQrvn9TXHB1x/w4= k8s.io/apimachinery v0.0.0-20220329130813-31e52c987dc1 h1:ESix+UawmXFu9oem7ngnHSbufLCiIQrvn9TXHB1x/w4=
k8s.io/apimachinery v0.0.0-20220329130813-31e52c987dc1/go.mod h1:WkN7hnr/sIpKTK8v3BZKqLkdqTMz00TBdMWqE0M0O7Q= k8s.io/apimachinery v0.0.0-20220329130813-31e52c987dc1/go.mod h1:WkN7hnr/sIpKTK8v3BZKqLkdqTMz00TBdMWqE0M0O7Q=
k8s.io/cli-runtime v0.0.0-20220329133126-053a01944821 h1:OX3/DwGpXAiDsiKqMpmQYPUX2nEFvdGk8TU+AeUht6c= k8s.io/cli-runtime v0.0.0-20220329133126-053a01944821 h1:OX3/DwGpXAiDsiKqMpmQYPUX2nEFvdGk8TU+AeUht6c=
k8s.io/cli-runtime v0.0.0-20220329133126-053a01944821/go.mod h1:lOklRIDTcen9/8KLP49hf8iHEJfqgJhVyBfG/bQDDMY= k8s.io/cli-runtime v0.0.0-20220329133126-053a01944821/go.mod h1:lOklRIDTcen9/8KLP49hf8iHEJfqgJhVyBfG/bQDDMY=
k8s.io/client-go v0.0.0-20220329091314-94a1081fafd8 h1:VmxceevfkORQc/j4wPYl+DxX4I3zgRYzNyL/NtbU9co= k8s.io/client-go v0.0.0-20220329211335-0c14d0c48569 h1:7RLx5MmoK4tGOkugeraoHIPzd3mwMXIYhnqvAKpkPx8=
k8s.io/client-go v0.0.0-20220329091314-94a1081fafd8/go.mod h1:r5XwHVMi90Hb2U6aj1EE2C8sHXRrkaFfgVBnTw889Ck= k8s.io/client-go v0.0.0-20220329211335-0c14d0c48569/go.mod h1:MeoFurfb4ZppznfLFCIjlE0y8sIIQkwa6N+2upsabj8=
k8s.io/code-generator v0.0.0-20220329130608-15a5f67a8af0/go.mod h1:K+f3PS8AJPkGO9NJoAqq6J1NMsr37Ey1IaQrQ2c97yQ= k8s.io/code-generator v0.0.0-20220329130608-15a5f67a8af0/go.mod h1:K+f3PS8AJPkGO9NJoAqq6J1NMsr37Ey1IaQrQ2c97yQ=
k8s.io/component-base v0.0.0-20220329051641-59f2f1feee51 h1:AI0vRA5cLetMNCGUpoPDsOeMmnjrh3kxvppvtN+osTw= k8s.io/component-base v0.0.0-20220329051641-59f2f1feee51 h1:AI0vRA5cLetMNCGUpoPDsOeMmnjrh3kxvppvtN+osTw=
k8s.io/component-base v0.0.0-20220329051641-59f2f1feee51/go.mod h1:URhW+V/OROyYt47UpSA+faMtPWsMecoLBv7efKyy1do= k8s.io/component-base v0.0.0-20220329051641-59f2f1feee51/go.mod h1:URhW+V/OROyYt47UpSA+faMtPWsMecoLBv7efKyy1do=

View File

@ -17,7 +17,9 @@ limitations under the License.
package expose package expose
import ( import (
"fmt"
"regexp" "regexp"
"strconv"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -25,16 +27,17 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta" "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"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/generate" "k8s.io/kubectl/pkg/generate"
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
"k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/polymorphichelpers"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util"
@ -83,6 +86,7 @@ var (
kubectl expose deployment nginx --port=80 --target-port=8000`)) kubectl expose deployment nginx --port=80 --target-port=8000`))
) )
// ExposeServiceOptions holds the options for kubectl expose command
type ExposeServiceOptions struct { type ExposeServiceOptions struct {
cmdutil.OverrideOptions cmdutil.OverrideOptions
@ -91,13 +95,31 @@ type ExposeServiceOptions struct {
PrintFlags *genericclioptions.PrintFlags PrintFlags *genericclioptions.PrintFlags
PrintObj printers.ResourcePrinterFunc 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 DryRunStrategy cmdutil.DryRunStrategy
DryRunVerifier *resource.QueryParamVerifier DryRunVerifier *resource.QueryParamVerifier
EnforceNamespace bool EnforceNamespace bool
fieldManager string fieldManager string
Generators func(string) map[string]generate.Generator
CanBeExposed polymorphichelpers.CanBeExposedFunc CanBeExposed polymorphichelpers.CanBeExposedFunc
MapBasedSelectorForObject func(runtime.Object) (string, error) MapBasedSelectorForObject func(runtime.Object) (string, error)
PortsForObject polymorphichelpers.PortsForObjectFunc PortsForObject polymorphichelpers.PortsForObjectFunc
@ -113,6 +135,8 @@ type ExposeServiceOptions struct {
genericclioptions.IOStreams genericclioptions.IOStreams
} }
// NewExposeServiceOptions creates a new ExposeServiceOptions and return a pointer to the
// struct
func NewExposeServiceOptions(ioStreams genericclioptions.IOStreams) *ExposeServiceOptions { func NewExposeServiceOptions(ioStreams genericclioptions.IOStreams) *ExposeServiceOptions {
return &ExposeServiceOptions{ return &ExposeServiceOptions{
RecordFlags: genericclioptions.NewRecordFlags(), 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 { func NewCmdExposeService(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
o := NewExposeServiceOptions(streams) o := NewExposeServiceOptions(streams)
@ -148,20 +173,17 @@ func NewCmdExposeService(f cmdutil.Factory, streams genericclioptions.IOStreams)
o.RecordFlags.AddFlags(cmd) o.RecordFlags.AddFlags(cmd)
o.PrintFlags.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().StringVar(&o.Protocol, "protocol", o.Protocol, i18n.T("The network protocol for the service to be created. Default is 'TCP'."))
cmd.Flags().String("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().String("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().String("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().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().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().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().StringVarP(&o.Labels, "labels", "l", o.Labels, "Labels to apply to the service created by this call.")
cmd.Flags().StringP("labels", "l", "", "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().String("container-port", "", i18n.T("Synonym for --target-port")) 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().MarkDeprecated("container-port", "--container-port will be removed in the future, please use --target-port instead") cmd.Flags().StringVar(&o.Name, "name", o.Name, i18n.T("The name for the newly created object."))
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().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().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().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."))
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."))
cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-expose") cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-expose")
o.AddOverrideFlags(cmd) o.AddOverrideFlags(cmd)
@ -172,6 +194,7 @@ func NewCmdExposeService(f cmdutil.Factory, streams genericclioptions.IOStreams)
return cmd return cmd
} }
// Complete loads data from the command line environment
func (o *ExposeServiceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { func (o *ExposeServiceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
var err error var err error
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
@ -197,7 +220,6 @@ func (o *ExposeServiceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) e
return err return err
} }
o.Generators = generateversioned.GeneratorFn
o.Builder = f.NewBuilder() o.Builder = f.NewBuilder()
o.ClientForMapping = f.ClientForMapping o.ClientForMapping = f.ClientForMapping
o.CanBeExposed = polymorphichelpers.CanBeExposedFn o.CanBeExposed = polymorphichelpers.CanBeExposedFn
@ -218,6 +240,8 @@ func (o *ExposeServiceOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) e
return err 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 { func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) error {
r := o.Builder. r := o.Builder.
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
@ -229,18 +253,9 @@ func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) erro
Do() Do()
err := r.Err() err := r.Err()
if err != nil { if err != nil {
return cmdutil.UsageErrorf(cmd, err.Error()) return err
} }
// 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)
}
names := generator.ParamNames()
err = r.Visit(func(info *resource.Info, err error) error { err = r.Visit(func(info *resource.Info, err error) error {
if err != nil { if err != nil {
return err return err
@ -251,98 +266,90 @@ func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) erro
return err return err
} }
params := generate.MakeParams(cmd, names)
name := info.Name name := info.Name
if len(name) > validation.DNS1035LabelMaxLength { if len(name) > validation.DNS1035LabelMaxLength {
name = 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 // For objects that need a pod selector, derive it from the exposed object in case a user
// didn't explicitly specify one via --selector // 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) s, err := o.MapBasedSelectorForObject(info.Object)
if err != nil { 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 // For objects that need a port, derive it from the exposed object in case a user
// didn't explicitly specify one via --port // 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) ports, err := o.PortsForObject(info.Object)
if err != nil { 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) { switch len(ports) {
case 0: case 0:
if !isHeadlessService { 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: case 1:
params["port"] = ports[0] o.Port = ports[0]
default: default:
params["ports"] = strings.Join(ports, ",") o.Ports = strings.Join(ports, ",")
} }
} }
// Always try to derive protocols from the exposed object, may use // Always try to derive protocols from the exposed object, may use
// different protocols for different ports. // different protocols for different ports.
if _, found := params["protocol"]; found { protocolsMap, err := o.ProtocolsForObject(info.Object)
protocolsMap, err := o.ProtocolsForObject(info.Object) if err != nil {
if err != nil { return fmt.Errorf("couldn't find protocol via introspection: %v", err)
return cmdutil.UsageErrorf(cmd, "couldn't find protocol via introspection: %v", err) }
} if protocols := generate.MakeProtocols(protocolsMap); !generate.IsZero(protocols) {
if protocols := generate.MakeProtocols(protocolsMap); !generate.IsZero(protocols) { o.Protocols = protocols
params["protocols"] = protocols
}
} }
if generate.IsZero(params["labels"]) { if len(o.Labels) == 0 {
labels, err := meta.NewAccessor().Labels(info.Object) labels, err := meta.NewAccessor().Labels(info.Object)
if err != nil { if err != nil {
return err return err
} }
params["labels"] = polymorphichelpers.MakeLabels(labels) o.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
} }
// Generate new object // Generate new object
object, err := generator.Generate(params) service, err := o.createService()
if err != nil { if err != nil {
return err return err
} }
object, err = o.NewOverrider(&corev1.Service{}).Apply(object) overrideService, err := o.NewOverrider(&corev1.Service{}).Apply(service)
if err != nil { if err != nil {
return err 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) klog.V(4).Infof("error recording current command: %v", err)
} }
if o.DryRunStrategy == cmdutil.DryRunClient { 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) 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 return err
} }
asUnstructured := &unstructured.Unstructured{} 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 return err
} }
gvks, _, err := unstructuredscheme.NewUnstructuredObjectTyper().ObjectKinds(asUnstructured) gvks, _, err := unstructuredscheme.NewUnstructuredObjectTyper().ObjectKinds(asUnstructured)
@ -379,3 +386,186 @@ func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) erro
} }
return nil 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
}

View File

@ -21,7 +21,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "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)
}
})
}
}