1
16
17 package expose
18
19 import (
20 "fmt"
21 "regexp"
22 "strconv"
23 "strings"
24
25 "github.com/spf13/cobra"
26 "k8s.io/klog/v2"
27
28 corev1 "k8s.io/api/core/v1"
29 "k8s.io/apimachinery/pkg/api/meta"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
33 "k8s.io/apimachinery/pkg/runtime"
34 "k8s.io/apimachinery/pkg/util/intstr"
35 "k8s.io/apimachinery/pkg/util/validation"
36 "k8s.io/cli-runtime/pkg/genericclioptions"
37 "k8s.io/cli-runtime/pkg/genericiooptions"
38 "k8s.io/cli-runtime/pkg/printers"
39 "k8s.io/cli-runtime/pkg/resource"
40 cmdutil "k8s.io/kubectl/pkg/cmd/util"
41 "k8s.io/kubectl/pkg/polymorphichelpers"
42 "k8s.io/kubectl/pkg/scheme"
43 "k8s.io/kubectl/pkg/util"
44 "k8s.io/kubectl/pkg/util/completion"
45 "k8s.io/kubectl/pkg/util/i18n"
46 "k8s.io/kubectl/pkg/util/templates"
47 )
48
49 var (
50 exposeResources = i18n.T(`pod (po), service (svc), replicationcontroller (rc), deployment (deploy), replicaset (rs)`)
51
52 exposeLong = templates.LongDesc(i18n.T(`
53 Expose a resource as a new Kubernetes service.
54
55 Looks up a deployment, service, replica set, replication controller or pod by name and uses the selector
56 for that resource as the selector for a new service on the specified port. A deployment or replica set
57 will be exposed as a service only if its selector is convertible to a selector that service supports,
58 i.e. when the selector contains only the matchLabels component. Note that if no port is specified via
59 --port and the exposed resource has multiple ports, all will be re-used by the new service. Also if no
60 labels are specified, the new service will re-use the labels from the resource it exposes.
61
62 Possible resources include (case insensitive):
63
64 `) + exposeResources)
65
66 exposeExample = templates.Examples(i18n.T(`
67 # Create a service for a replicated nginx, which serves on port 80 and connects to the containers on port 8000
68 kubectl expose rc nginx --port=80 --target-port=8000
69
70 # Create a service for a replication controller identified by type and name specified in "nginx-controller.yaml", which serves on port 80 and connects to the containers on port 8000
71 kubectl expose -f nginx-controller.yaml --port=80 --target-port=8000
72
73 # Create a service for a pod valid-pod, which serves on port 444 with the name "frontend"
74 kubectl expose pod valid-pod --port=444 --name=frontend
75
76 # Create a second service based on the above service, exposing the container port 8443 as port 443 with the name "nginx-https"
77 kubectl expose service nginx --port=443 --target-port=8443 --name=nginx-https
78
79 # Create a service for a replicated streaming application on port 4100 balancing UDP traffic and named 'video-stream'.
80 kubectl expose rc streamer --port=4100 --protocol=UDP --name=video-stream
81
82 # Create a service for a replicated nginx using replica set, which serves on port 80 and connects to the containers on port 8000
83 kubectl expose rs nginx --port=80 --target-port=8000
84
85 # Create a service for an nginx deployment, which serves on port 80 and connects to the containers on port 8000
86 kubectl expose deployment nginx --port=80 --target-port=8000`))
87 )
88
89
90 type ExposeServiceOptions struct {
91 cmdutil.OverrideOptions
92
93 FilenameOptions resource.FilenameOptions
94 RecordFlags *genericclioptions.RecordFlags
95 PrintFlags *genericclioptions.PrintFlags
96 PrintObj printers.ResourcePrinterFunc
97
98 Name string
99 DefaultName string
100 Selector string
101
102 Port string
103
104 Ports string
105 Labels string
106 ExternalIP string
107 LoadBalancerIP string
108 Type string
109 Protocol string
110
111 Protocols string
112 TargetPort string
113 PortName string
114 SessionAffinity string
115 ClusterIP string
116
117 DryRunStrategy cmdutil.DryRunStrategy
118 EnforceNamespace bool
119
120 fieldManager string
121
122 CanBeExposed polymorphichelpers.CanBeExposedFunc
123 MapBasedSelectorForObject func(runtime.Object) (string, error)
124 PortsForObject polymorphichelpers.PortsForObjectFunc
125 ProtocolsForObject polymorphichelpers.MultiProtocolsWithForObjectFunc
126
127 Namespace string
128 Mapper meta.RESTMapper
129
130 Builder *resource.Builder
131 ClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error)
132
133 Recorder genericclioptions.Recorder
134 genericiooptions.IOStreams
135 }
136
137
138 type ExposeServiceFlags struct {
139 cmdutil.OverrideOptions
140 PrintFlags *genericclioptions.PrintFlags
141 RecordFlags *genericclioptions.RecordFlags
142
143 fieldManager string
144 Protocol string
145
146
147 Port string
148 Type string
149 LoadBalancerIP string
150 Selector string
151 Labels string
152 TargetPort string
153 ExternalIP string
154 Name string
155 SessionAffinity string
156 ClusterIP string
157 Recorder genericclioptions.Recorder
158 FilenameOptions resource.FilenameOptions
159 genericiooptions.IOStreams
160 }
161
162 func NewExposeFlags(ioStreams genericiooptions.IOStreams) *ExposeServiceFlags {
163 return &ExposeServiceFlags{
164 RecordFlags: genericclioptions.NewRecordFlags(),
165 PrintFlags: genericclioptions.NewPrintFlags("exposed").WithTypeSetter(scheme.Scheme),
166
167 Recorder: genericclioptions.NoopRecorder{},
168 IOStreams: ioStreams,
169 }
170 }
171
172
173 func NewCmdExposeService(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
174 flags := NewExposeFlags(streams)
175
176 validArgs := []string{}
177 resources := regexp.MustCompile(`\s*,`).Split(exposeResources, -1)
178 for _, r := range resources {
179 validArgs = append(validArgs, strings.Fields(r)[0])
180 }
181
182 cmd := &cobra.Command{
183 Use: "expose (-f FILENAME | TYPE NAME) [--port=port] [--protocol=TCP|UDP|SCTP] [--target-port=number-or-name] [--name=name] [--external-ip=external-ip-of-service] [--type=type]",
184 DisableFlagsInUseLine: true,
185 Short: i18n.T("Take a replication controller, service, deployment or pod and expose it as a new Kubernetes service"),
186 Long: exposeLong,
187 Example: exposeExample,
188 ValidArgsFunction: completion.SpecifiedResourceTypeAndNameCompletionFunc(f, validArgs),
189 Run: func(cmd *cobra.Command, args []string) {
190 o, err := flags.ToOptions(cmd, args)
191 cmdutil.CheckErr(err)
192 cmdutil.CheckErr(o.Complete(f))
193 cmdutil.CheckErr(o.RunExpose(cmd, args))
194 },
195 }
196
197 flags.AddFlags(cmd)
198 return cmd
199 }
200
201 func (flags *ExposeServiceFlags) AddFlags(cmd *cobra.Command) {
202 flags.PrintFlags.AddFlags(cmd)
203 flags.RecordFlags.AddFlags(cmd)
204
205 cmd.Flags().StringVar(&flags.Protocol, "protocol", flags.Protocol, i18n.T("The network protocol for the service to be created. Default is 'TCP'."))
206 cmd.Flags().StringVar(&flags.Port, "port", flags.Port, i18n.T("The port that the service should serve on. Copied from the resource being exposed, if unspecified"))
207 cmd.Flags().StringVar(&flags.Type, "type", flags.Type, i18n.T("Type for this service: ClusterIP, NodePort, LoadBalancer, or ExternalName. Default is 'ClusterIP'."))
208 cmd.Flags().StringVar(&flags.LoadBalancerIP, "load-balancer-ip", flags.LoadBalancerIP, i18n.T("IP to assign to the LoadBalancer. If empty, an ephemeral IP will be created and used (cloud-provider specific)."))
209 cmd.Flags().StringVar(&flags.Selector, "selector", flags.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.)"))
210 cmd.Flags().StringVarP(&flags.Labels, "labels", "l", flags.Labels, "Labels to apply to the service created by this call.")
211 cmd.Flags().StringVar(&flags.TargetPort, "target-port", flags.TargetPort, i18n.T("Name or number for the port on the container that the service should direct traffic to. Optional."))
212 cmd.Flags().StringVar(&flags.ExternalIP, "external-ip", flags.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."))
213 cmd.Flags().StringVar(&flags.Name, "name", flags.Name, i18n.T("The name for the newly created object."))
214 cmd.Flags().StringVar(&flags.SessionAffinity, "session-affinity", flags.SessionAffinity, i18n.T("If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP'"))
215 cmd.Flags().StringVar(&flags.ClusterIP, "cluster-ip", flags.ClusterIP, i18n.T("ClusterIP to be assigned to the service. Leave empty to auto-allocate, or set to 'None' to create a headless service."))
216
217 cmdutil.AddFieldManagerFlagVar(cmd, &flags.fieldManager, "kubectl-expose")
218 flags.AddOverrideFlags(cmd)
219
220 cmdutil.AddDryRunFlag(cmd)
221 cmdutil.AddApplyAnnotationFlags(cmd)
222
223 usage := "identifying the resource to expose a service"
224 cmdutil.AddFilenameOptionFlags(cmd, &flags.FilenameOptions, usage)
225 }
226
227 func (flags *ExposeServiceFlags) ToOptions(cmd *cobra.Command, args []string) (*ExposeServiceOptions, error) {
228 dryRunStratergy, err := cmdutil.GetDryRunStrategy(cmd)
229 if err != nil {
230 return nil, err
231 }
232
233 cmdutil.PrintFlagsWithDryRunStrategy(flags.PrintFlags, dryRunStratergy)
234 printer, err := flags.PrintFlags.ToPrinter()
235 if err != nil {
236 return nil, err
237 }
238
239 flags.RecordFlags.Complete(cmd)
240 recorder, err := flags.RecordFlags.ToRecorder()
241 if err != nil {
242 return nil, err
243 }
244
245 e := &ExposeServiceOptions{
246 DryRunStrategy: dryRunStratergy,
247 PrintObj: printer.PrintObj,
248 Recorder: recorder,
249 IOStreams: flags.IOStreams,
250 fieldManager: flags.fieldManager,
251 PrintFlags: flags.PrintFlags,
252 RecordFlags: flags.RecordFlags,
253 FilenameOptions: flags.FilenameOptions,
254 Protocol: flags.Protocol,
255 Port: flags.Port,
256 Type: flags.Type,
257 LoadBalancerIP: flags.LoadBalancerIP,
258 Selector: flags.Selector,
259 Labels: flags.Labels,
260 TargetPort: flags.TargetPort,
261 ExternalIP: flags.ExternalIP,
262 Name: flags.Name,
263 SessionAffinity: flags.SessionAffinity,
264 ClusterIP: flags.ClusterIP,
265 OverrideOptions: flags.OverrideOptions,
266 }
267 return e, nil
268 }
269
270
271 func (o *ExposeServiceOptions) Complete(f cmdutil.Factory) error {
272 var err error
273
274 o.Builder = f.NewBuilder()
275 o.ClientForMapping = f.ClientForMapping
276 o.CanBeExposed = polymorphichelpers.CanBeExposedFn
277 o.MapBasedSelectorForObject = polymorphichelpers.MapBasedSelectorForObjectFn
278 o.ProtocolsForObject = polymorphichelpers.MultiProtocolsForObjectFn
279 o.PortsForObject = polymorphichelpers.PortsForObjectFn
280
281 o.Mapper, err = f.ToRESTMapper()
282 if err != nil {
283 return err
284 }
285
286 o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
287 if err != nil {
288 return err
289 }
290
291 return err
292 }
293
294
295
296 func (o *ExposeServiceOptions) RunExpose(cmd *cobra.Command, args []string) error {
297 r := o.Builder.
298 WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
299 ContinueOnError().
300 NamespaceParam(o.Namespace).DefaultNamespace().
301 FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
302 ResourceTypeOrNameArgs(false, args...).
303 Flatten().
304 Do()
305 err := r.Err()
306 if err != nil {
307 return err
308 }
309
310 err = r.Visit(func(info *resource.Info, err error) error {
311 if err != nil {
312 return err
313 }
314
315 mapping := info.ResourceMapping()
316 if err := o.CanBeExposed(mapping.GroupVersionKind.GroupKind()); err != nil {
317 return err
318 }
319
320 name := info.Name
321 if len(name) > validation.DNS1035LabelMaxLength {
322 name = name[:validation.DNS1035LabelMaxLength]
323 }
324 o.DefaultName = name
325
326
327
328 if len(o.Selector) == 0 {
329 s, err := o.MapBasedSelectorForObject(info.Object)
330 if err != nil {
331 return fmt.Errorf("couldn't retrieve selectors via --selector flag or introspection: %v", err)
332 }
333 o.Selector = s
334 }
335
336 isHeadlessService := o.ClusterIP == "None"
337
338
339
340 if len(o.Port) == 0 {
341 ports, err := o.PortsForObject(info.Object)
342 if err != nil {
343 return fmt.Errorf("couldn't find port via --port flag or introspection: %v", err)
344 }
345 switch len(ports) {
346 case 0:
347 if !isHeadlessService {
348 return fmt.Errorf("couldn't find port via --port flag or introspection")
349 }
350 case 1:
351 o.Port = ports[0]
352 default:
353 o.Ports = strings.Join(ports, ",")
354 }
355 }
356
357
358
359 protocolsMap, err := o.ProtocolsForObject(info.Object)
360 if err != nil {
361 return fmt.Errorf("couldn't find protocol via introspection: %v", err)
362 }
363 if protocols := makeProtocols(protocolsMap); len(protocols) > 0 {
364 o.Protocols = protocols
365 }
366
367 if len(o.Labels) == 0 {
368 labels, err := meta.NewAccessor().Labels(info.Object)
369 if err != nil {
370 return err
371 }
372 o.Labels = polymorphichelpers.MakeLabels(labels)
373 }
374
375
376 service, err := o.createService()
377 if err != nil {
378 return err
379 }
380
381 overrideService, err := o.NewOverrider(&corev1.Service{}).Apply(service)
382 if err != nil {
383 return err
384 }
385
386 if err := o.Recorder.Record(overrideService); err != nil {
387 klog.V(4).Infof("error recording current command: %v", err)
388 }
389
390 if o.DryRunStrategy == cmdutil.DryRunClient {
391 if meta, err := meta.Accessor(overrideService); err == nil && o.EnforceNamespace {
392 meta.SetNamespace(o.Namespace)
393 }
394 return o.PrintObj(overrideService, o.Out)
395 }
396 if err := util.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), overrideService, scheme.DefaultJSONEncoder()); err != nil {
397 return err
398 }
399
400 asUnstructured := &unstructured.Unstructured{}
401 if err := scheme.Scheme.Convert(overrideService, asUnstructured, nil); err != nil {
402 return err
403 }
404 gvks, _, err := unstructuredscheme.NewUnstructuredObjectTyper().ObjectKinds(asUnstructured)
405 if err != nil {
406 return err
407 }
408 objMapping, err := o.Mapper.RESTMapping(gvks[0].GroupKind(), gvks[0].Version)
409 if err != nil {
410 return err
411 }
412
413 client, err := o.ClientForMapping(objMapping)
414 if err != nil {
415 return err
416 }
417 actualObject, err := resource.
418 NewHelper(client, objMapping).
419 DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
420 WithFieldManager(o.fieldManager).
421 Create(o.Namespace, false, asUnstructured)
422 if err != nil {
423 return err
424 }
425 return o.PrintObj(actualObject, o.Out)
426 })
427 return err
428 }
429
430 func (o *ExposeServiceOptions) createService() (*corev1.Service, error) {
431 if len(o.Selector) == 0 {
432 return nil, fmt.Errorf("selector must be specified")
433 }
434 selector, err := parseLabels(o.Selector)
435 if err != nil {
436 return nil, err
437 }
438
439 var labels map[string]string
440 if len(o.Labels) > 0 {
441 labels, err = parseLabels(o.Labels)
442 if err != nil {
443 return nil, err
444 }
445 }
446
447 name := o.Name
448 if len(name) == 0 {
449 name = o.DefaultName
450 if len(name) == 0 {
451 return nil, fmt.Errorf("name must be specified")
452 }
453 }
454
455 var portProtocolMap map[string][]string
456 if o.Protocols != "" {
457 portProtocolMap, err = parseProtocols(o.Protocols)
458 if err != nil {
459 return nil, err
460 }
461 }
462
463
464
465
466 var portString string
467 portString = o.Ports
468 if len(o.Ports) == 0 {
469 portString = o.Port
470 }
471
472 ports := []corev1.ServicePort{}
473 if len(portString) != 0 {
474 portStringSlice := strings.Split(portString, ",")
475 servicePortName := o.PortName
476 for i, stillPortString := range portStringSlice {
477 port, err := strconv.Atoi(stillPortString)
478 if err != nil {
479 return nil, err
480 }
481 name := servicePortName
482
483
484 if len(portStringSlice) > 1 {
485 name = fmt.Sprintf("port-%d", i+1)
486 }
487 protocol := o.Protocol
488
489 switch {
490 case len(protocol) == 0 && len(portProtocolMap) == 0:
491
492 protocol = "TCP"
493 case len(protocol) > 0 && len(portProtocolMap) > 0:
494
495
496 case len(protocol) == 0 && len(portProtocolMap) > 0:
497
498 protocol = "TCP"
499 if exposeProtocols, found := portProtocolMap[stillPortString]; found {
500 if len(exposeProtocols) == 1 {
501 protocol = exposeProtocols[0]
502 break
503 }
504 for _, exposeProtocol := range exposeProtocols {
505 name := fmt.Sprintf("port-%d-%s", i+1, strings.ToLower(exposeProtocol))
506 ports = append(ports, corev1.ServicePort{
507 Name: name,
508 Port: int32(port),
509 Protocol: corev1.Protocol(exposeProtocol),
510 })
511 }
512 continue
513 }
514 }
515 ports = append(ports, corev1.ServicePort{
516 Name: name,
517 Port: int32(port),
518 Protocol: corev1.Protocol(protocol),
519 })
520 }
521 }
522
523 service := corev1.Service{
524 ObjectMeta: metav1.ObjectMeta{
525 Name: name,
526 Labels: labels,
527 },
528 Spec: corev1.ServiceSpec{
529 Selector: selector,
530 Ports: ports,
531 },
532 }
533 targetPortString := o.TargetPort
534 if len(targetPortString) > 0 {
535 targetPort := intstr.Parse(targetPortString)
536
537 for i := range service.Spec.Ports {
538 service.Spec.Ports[i].TargetPort = targetPort
539 }
540 } else {
541
542
543 for i := range service.Spec.Ports {
544 port := service.Spec.Ports[i].Port
545 service.Spec.Ports[i].TargetPort = intstr.FromInt32(port)
546 }
547 }
548 if len(o.ExternalIP) > 0 {
549 service.Spec.ExternalIPs = []string{o.ExternalIP}
550 }
551 if len(o.Type) != 0 {
552 service.Spec.Type = corev1.ServiceType(o.Type)
553 }
554 if service.Spec.Type == corev1.ServiceTypeLoadBalancer {
555 service.Spec.LoadBalancerIP = o.LoadBalancerIP
556 }
557 if len(o.SessionAffinity) != 0 {
558 switch corev1.ServiceAffinity(o.SessionAffinity) {
559 case corev1.ServiceAffinityNone:
560 service.Spec.SessionAffinity = corev1.ServiceAffinityNone
561 case corev1.ServiceAffinityClientIP:
562 service.Spec.SessionAffinity = corev1.ServiceAffinityClientIP
563 default:
564 return nil, fmt.Errorf("unknown session affinity: %s", o.SessionAffinity)
565 }
566 }
567 if len(o.ClusterIP) != 0 {
568 if o.ClusterIP == "None" {
569 service.Spec.ClusterIP = corev1.ClusterIPNone
570 } else {
571 service.Spec.ClusterIP = o.ClusterIP
572 }
573 }
574 return &service, nil
575 }
576
577
578 func parseLabels(labelSpec string) (map[string]string, error) {
579 if len(labelSpec) == 0 {
580 return nil, fmt.Errorf("no label spec passed")
581 }
582 labels := map[string]string{}
583 labelSpecs := strings.Split(labelSpec, ",")
584 for ix := range labelSpecs {
585 labelSpec := strings.Split(labelSpecs[ix], "=")
586 if len(labelSpec) != 2 {
587 return nil, fmt.Errorf("unexpected label spec: %s", labelSpecs[ix])
588 }
589 if len(labelSpec[0]) == 0 {
590 return nil, fmt.Errorf("unexpected empty label key")
591 }
592 labels[labelSpec[0]] = labelSpec[1]
593 }
594 return labels, nil
595 }
596
597 func makeProtocols(protocols map[string][]string) string {
598 var out []string
599 for key, value := range protocols {
600 for _, s := range value {
601 out = append(out, fmt.Sprintf("%s/%s", key, s))
602 }
603 }
604 return strings.Join(out, ",")
605 }
606
607
608 func parseProtocols(protocols string) (map[string][]string, error) {
609 if len(protocols) == 0 {
610 return nil, fmt.Errorf("no protocols passed")
611 }
612 portProtocolMap := map[string][]string{}
613 protocolsSlice := strings.Split(protocols, ",")
614 for ix := range protocolsSlice {
615 portProtocol := strings.Split(protocolsSlice[ix], "/")
616 if len(portProtocol) != 2 {
617 return nil, fmt.Errorf("unexpected port protocol mapping: %s", protocolsSlice[ix])
618 }
619 if len(portProtocol[0]) == 0 {
620 return nil, fmt.Errorf("unexpected empty port")
621 }
622 if len(portProtocol[1]) == 0 {
623 return nil, fmt.Errorf("unexpected empty protocol")
624 }
625 port := portProtocol[0]
626 portProtocolMap[port] = append(portProtocolMap[port], portProtocol[1])
627 }
628 return portProtocolMap, nil
629 }
630
View as plain text