...

Source file src/k8s.io/kubectl/pkg/cmd/create/create_ingress.go

Documentation: k8s.io/kubectl/pkg/cmd/create

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package create
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"regexp"
    23  	"strings"
    24  
    25  	"github.com/spf13/cobra"
    26  
    27  	networkingv1 "k8s.io/api/networking/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/util/intstr"
    31  	"k8s.io/cli-runtime/pkg/genericclioptions"
    32  	"k8s.io/cli-runtime/pkg/genericiooptions"
    33  	networkingv1client "k8s.io/client-go/kubernetes/typed/networking/v1"
    34  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    35  	"k8s.io/kubectl/pkg/scheme"
    36  	"k8s.io/kubectl/pkg/util"
    37  	"k8s.io/kubectl/pkg/util/i18n"
    38  	"k8s.io/kubectl/pkg/util/templates"
    39  )
    40  
    41  var (
    42  	// Explaining the Regex below:
    43  	// ^(?P<host>[\w\*\-\.]*) -> Indicates the host - 0-N characters of letters, number, underscore, '-', '.' and '*'
    44  	// (?P<path>/.*) -> Indicates the path and MUST start with '/' - / + 0-N characters
    45  	// Separator from host/path to svcname:svcport -> "="
    46  	// (?P<svcname>[\w\-]+) -> Service Name (letters, numbers, '-') -> 1-N characters
    47  	// Separator from svcname to svcport -> ":"
    48  	// (?P<svcport>[\w\-]+) -> Service Port (letters, numbers, '-') -> 1-N characters
    49  	regexHostPathSvc = `^(?P<host>[\w\*\-\.]*)(?P<path>/.*)=(?P<svcname>[\w\-]+):(?P<svcport>[\w\-]+)`
    50  
    51  	// This Regex is optional -> (....)?
    52  	// (?P<istls>tls) -> Verify if the argument after "," is 'tls'
    53  	// Optional Separator from tls to the secret name -> "=?"
    54  	// (?P<secretname>[\w\-]+)? -> Optional secret name after the separator -> 1-N characters
    55  	regexTLS = `(,(?P<istls>tls)=?(?P<secretname>[\w\-]+)?)?`
    56  
    57  	// The validation Regex is the concatenation of hostPathSvc validation regex
    58  	// and the TLS validation regex
    59  	ruleRegex = regexHostPathSvc + regexTLS
    60  
    61  	ingressLong = templates.LongDesc(i18n.T(`
    62  	Create an ingress with the specified name.`))
    63  
    64  	ingressExample = templates.Examples(i18n.T(`
    65  		# Create a single ingress called 'simple' that directs requests to foo.com/bar to svc
    66  		# svc1:8080 with a TLS secret "my-cert"
    67  		kubectl create ingress simple --rule="foo.com/bar=svc1:8080,tls=my-cert"
    68  
    69  		# Create a catch all ingress of "/path" pointing to service svc:port and Ingress Class as "otheringress"
    70  		kubectl create ingress catch-all --class=otheringress --rule="/path=svc:port"
    71  
    72  		# Create an ingress with two annotations: ingress.annotation1 and ingress.annotations2
    73  		kubectl create ingress annotated --class=default --rule="foo.com/bar=svc:port" \
    74  			--annotation ingress.annotation1=foo \
    75  			--annotation ingress.annotation2=bla
    76  
    77  		# Create an ingress with the same host and multiple paths
    78  		kubectl create ingress multipath --class=default \
    79  			--rule="foo.com/=svc:port" \
    80  			--rule="foo.com/admin/=svcadmin:portadmin"
    81  
    82  		# Create an ingress with multiple hosts and the pathType as Prefix
    83  		kubectl create ingress ingress1 --class=default \
    84  			--rule="foo.com/path*=svc:8080" \
    85  			--rule="bar.com/admin*=svc2:http"
    86  
    87  		# Create an ingress with TLS enabled using the default ingress certificate and different path types
    88  		kubectl create ingress ingtls --class=default \
    89  		   --rule="foo.com/=svc:https,tls" \
    90  		   --rule="foo.com/path/subpath*=othersvc:8080"
    91  
    92  		# Create an ingress with TLS enabled using a specific secret and pathType as Prefix
    93  		kubectl create ingress ingsecret --class=default \
    94  		   --rule="foo.com/*=svc:8080,tls=secret1"
    95  
    96  		# Create an ingress with a default backend
    97  		kubectl create ingress ingdefault --class=default \
    98  		   --default-backend=defaultsvc:http \
    99  		   --rule="foo.com/*=svc:8080,tls=secret1"
   100  
   101  		`))
   102  )
   103  
   104  // CreateIngressOptions is returned by NewCmdCreateIngress
   105  type CreateIngressOptions struct {
   106  	PrintFlags *genericclioptions.PrintFlags
   107  
   108  	PrintObj func(obj runtime.Object) error
   109  
   110  	Name             string
   111  	IngressClass     string
   112  	Rules            []string
   113  	Annotations      []string
   114  	DefaultBackend   string
   115  	Namespace        string
   116  	EnforceNamespace bool
   117  	CreateAnnotation bool
   118  
   119  	Client              networkingv1client.NetworkingV1Interface
   120  	DryRunStrategy      cmdutil.DryRunStrategy
   121  	ValidationDirective string
   122  
   123  	FieldManager string
   124  
   125  	genericiooptions.IOStreams
   126  }
   127  
   128  // NewCreateIngressOptions creates the CreateIngressOptions to be used later
   129  func NewCreateIngressOptions(ioStreams genericiooptions.IOStreams) *CreateIngressOptions {
   130  	return &CreateIngressOptions{
   131  		PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
   132  		IOStreams:  ioStreams,
   133  	}
   134  }
   135  
   136  // NewCmdCreateIngress is a macro command to create a new ingress.
   137  // This command is better known to users as `kubectl create ingress`.
   138  func NewCmdCreateIngress(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
   139  	o := NewCreateIngressOptions(ioStreams)
   140  
   141  	cmd := &cobra.Command{
   142  		Use:                   "ingress NAME --rule=host/path=service:port[,tls[=secret]] ",
   143  		DisableFlagsInUseLine: true,
   144  		Aliases:               []string{"ing"},
   145  		Short:                 i18n.T("Create an ingress with the specified name"),
   146  		Long:                  ingressLong,
   147  		Example:               ingressExample,
   148  		Run: func(cmd *cobra.Command, args []string) {
   149  			cmdutil.CheckErr(o.Complete(f, cmd, args))
   150  			cmdutil.CheckErr(o.Validate())
   151  			cmdutil.CheckErr(o.Run())
   152  		},
   153  	}
   154  
   155  	o.PrintFlags.AddFlags(cmd)
   156  
   157  	cmdutil.AddApplyAnnotationFlags(cmd)
   158  	cmdutil.AddValidateFlags(cmd)
   159  	cmdutil.AddDryRunFlag(cmd)
   160  	cmd.Flags().StringVar(&o.IngressClass, "class", o.IngressClass, "Ingress Class to be used")
   161  	cmd.Flags().StringArrayVar(&o.Rules, "rule", o.Rules, "Rule in format host/path=service:port[,tls=secretname]. Paths containing the leading character '*' are considered pathType=Prefix. tls argument is optional.")
   162  	cmd.Flags().StringVar(&o.DefaultBackend, "default-backend", o.DefaultBackend, "Default service for backend, in format of svcname:port")
   163  	cmd.Flags().StringArrayVar(&o.Annotations, "annotation", o.Annotations, "Annotation to insert in the ingress object, in the format annotation=value")
   164  	cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
   165  
   166  	return cmd
   167  }
   168  
   169  // Complete completes all the options
   170  func (o *CreateIngressOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
   171  	name, err := NameFromCommandArgs(cmd, args)
   172  	if err != nil {
   173  		return err
   174  	}
   175  	o.Name = name
   176  
   177  	clientConfig, err := f.ToRESTConfig()
   178  	if err != nil {
   179  		return err
   180  	}
   181  	o.Client, err = networkingv1client.NewForConfig(clientConfig)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
   192  
   193  	o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
   194  	if err != nil {
   195  		return err
   196  	}
   197  	cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
   198  
   199  	printer, err := o.PrintFlags.ToPrinter()
   200  	if err != nil {
   201  		return err
   202  	}
   203  	o.PrintObj = func(obj runtime.Object) error {
   204  		return printer.PrintObj(obj, o.Out)
   205  	}
   206  
   207  	o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd)
   208  	return err
   209  }
   210  
   211  // Validate validates the Ingress object to be created
   212  func (o *CreateIngressOptions) Validate() error {
   213  	if len(o.DefaultBackend) == 0 && len(o.Rules) == 0 {
   214  		return fmt.Errorf("not enough information provided: every ingress has to either specify a default-backend (which catches all traffic) or a list of rules (which catch specific paths)")
   215  	}
   216  
   217  	rulevalidation, err := regexp.Compile(ruleRegex)
   218  	if err != nil {
   219  		return fmt.Errorf("failed to compile the regex")
   220  	}
   221  
   222  	for _, rule := range o.Rules {
   223  		if match := rulevalidation.MatchString(rule); !match {
   224  			return fmt.Errorf("rule %s is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", rule)
   225  		}
   226  	}
   227  
   228  	for _, annotation := range o.Annotations {
   229  		if an := strings.SplitN(annotation, "=", 2); len(an) != 2 {
   230  			return fmt.Errorf("annotation %s is invalid and should be in format key=[value]", annotation)
   231  		}
   232  	}
   233  
   234  	if len(o.DefaultBackend) > 0 && len(strings.Split(o.DefaultBackend, ":")) != 2 {
   235  		return fmt.Errorf("default-backend should be in format servicename:serviceport")
   236  	}
   237  
   238  	return nil
   239  }
   240  
   241  // Run performs the execution of 'create ingress' sub command
   242  func (o *CreateIngressOptions) Run() error {
   243  	ingress := o.createIngress()
   244  
   245  	if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, ingress, scheme.DefaultJSONEncoder()); err != nil {
   246  		return err
   247  	}
   248  
   249  	if o.DryRunStrategy != cmdutil.DryRunClient {
   250  		createOptions := metav1.CreateOptions{}
   251  		if o.FieldManager != "" {
   252  			createOptions.FieldManager = o.FieldManager
   253  		}
   254  		createOptions.FieldValidation = o.ValidationDirective
   255  		if o.DryRunStrategy == cmdutil.DryRunServer {
   256  			createOptions.DryRun = []string{metav1.DryRunAll}
   257  		}
   258  		var err error
   259  		ingress, err = o.Client.Ingresses(o.Namespace).Create(context.TODO(), ingress, createOptions)
   260  		if err != nil {
   261  			return fmt.Errorf("failed to create ingress: %v", err)
   262  		}
   263  	}
   264  	return o.PrintObj(ingress)
   265  }
   266  
   267  func (o *CreateIngressOptions) createIngress() *networkingv1.Ingress {
   268  	namespace := ""
   269  	if o.EnforceNamespace {
   270  		namespace = o.Namespace
   271  	}
   272  
   273  	annotations := o.buildAnnotations()
   274  	spec := o.buildIngressSpec()
   275  
   276  	ingress := &networkingv1.Ingress{
   277  		TypeMeta: metav1.TypeMeta{APIVersion: networkingv1.SchemeGroupVersion.String(), Kind: "Ingress"},
   278  		ObjectMeta: metav1.ObjectMeta{
   279  			Name:        o.Name,
   280  			Namespace:   namespace,
   281  			Annotations: annotations,
   282  		},
   283  		Spec: spec,
   284  	}
   285  	return ingress
   286  }
   287  
   288  func (o *CreateIngressOptions) buildAnnotations() map[string]string {
   289  
   290  	var annotations = make(map[string]string)
   291  
   292  	for _, annotation := range o.Annotations {
   293  		an := strings.SplitN(annotation, "=", 2)
   294  		annotations[an[0]] = an[1]
   295  	}
   296  	return annotations
   297  }
   298  
   299  // buildIngressSpec builds the .spec from the diverse arguments passed to kubectl
   300  func (o *CreateIngressOptions) buildIngressSpec() networkingv1.IngressSpec {
   301  	var ingressSpec networkingv1.IngressSpec
   302  
   303  	if len(o.IngressClass) > 0 {
   304  		ingressSpec.IngressClassName = &o.IngressClass
   305  	}
   306  
   307  	if len(o.DefaultBackend) > 0 {
   308  		defaultbackend := buildIngressBackendSvc(o.DefaultBackend)
   309  		ingressSpec.DefaultBackend = &defaultbackend
   310  	}
   311  	ingressSpec.TLS = o.buildTLSRules()
   312  	ingressSpec.Rules = o.buildIngressRules()
   313  
   314  	return ingressSpec
   315  }
   316  
   317  func (o *CreateIngressOptions) buildTLSRules() []networkingv1.IngressTLS {
   318  	hostAlreadyPresent := make(map[string]struct{})
   319  
   320  	ingressTLSs := []networkingv1.IngressTLS{}
   321  	var secret string
   322  
   323  	for _, rule := range o.Rules {
   324  		tls := strings.Split(rule, ",")
   325  
   326  		if len(tls) == 2 {
   327  			ingressTLS := networkingv1.IngressTLS{}
   328  			host := strings.SplitN(rule, "/", 2)[0]
   329  			secret = ""
   330  			secretName := strings.Split(tls[1], "=")
   331  
   332  			if len(secretName) > 1 {
   333  				secret = secretName[1]
   334  			}
   335  
   336  			idxSecret := getIndexSecret(secret, ingressTLSs)
   337  			// We accept the same host into TLS secrets only once
   338  			if _, ok := hostAlreadyPresent[host]; !ok {
   339  				if idxSecret > -1 {
   340  					ingressTLSs[idxSecret].Hosts = append(ingressTLSs[idxSecret].Hosts, host)
   341  					hostAlreadyPresent[host] = struct{}{}
   342  					continue
   343  				}
   344  				if host != "" {
   345  					ingressTLS.Hosts = append(ingressTLS.Hosts, host)
   346  				}
   347  				if secret != "" {
   348  					ingressTLS.SecretName = secret
   349  				}
   350  				if len(ingressTLS.SecretName) > 0 || len(ingressTLS.Hosts) > 0 {
   351  					ingressTLSs = append(ingressTLSs, ingressTLS)
   352  				}
   353  				hostAlreadyPresent[host] = struct{}{}
   354  			}
   355  		}
   356  	}
   357  	return ingressTLSs
   358  }
   359  
   360  // buildIngressRules builds the .spec.rules for an ingress object.
   361  func (o *CreateIngressOptions) buildIngressRules() []networkingv1.IngressRule {
   362  	ingressRules := []networkingv1.IngressRule{}
   363  
   364  	for _, rule := range o.Rules {
   365  		removeTLS := strings.Split(rule, ",")[0]
   366  		hostSplit := strings.SplitN(removeTLS, "/", 2)
   367  		host := hostSplit[0]
   368  		ingressPath := buildHTTPIngressPath(hostSplit[1])
   369  		ingressRule := networkingv1.IngressRule{}
   370  
   371  		if host != "" {
   372  			ingressRule.Host = host
   373  		}
   374  
   375  		idxHost := getIndexHost(ingressRule.Host, ingressRules)
   376  		if idxHost > -1 {
   377  			ingressRules[idxHost].IngressRuleValue.HTTP.Paths = append(ingressRules[idxHost].IngressRuleValue.HTTP.Paths, ingressPath)
   378  			continue
   379  		}
   380  
   381  		ingressRule.IngressRuleValue = networkingv1.IngressRuleValue{
   382  			HTTP: &networkingv1.HTTPIngressRuleValue{
   383  				Paths: []networkingv1.HTTPIngressPath{
   384  					ingressPath,
   385  				},
   386  			},
   387  		}
   388  		ingressRules = append(ingressRules, ingressRule)
   389  	}
   390  	return ingressRules
   391  }
   392  
   393  func buildHTTPIngressPath(pathsvc string) networkingv1.HTTPIngressPath {
   394  	pathsvcsplit := strings.Split(pathsvc, "=")
   395  	path := "/" + pathsvcsplit[0]
   396  	service := pathsvcsplit[1]
   397  
   398  	var pathType networkingv1.PathType
   399  	pathType = "Exact"
   400  
   401  	// If * in the End, turn pathType=Prefix but remove the * from the end
   402  	if path[len(path)-1:] == "*" {
   403  		pathType = "Prefix"
   404  		path = path[0 : len(path)-1]
   405  	}
   406  
   407  	httpIngressPath := networkingv1.HTTPIngressPath{
   408  		Path:     path,
   409  		PathType: &pathType,
   410  		Backend:  buildIngressBackendSvc(service),
   411  	}
   412  	return httpIngressPath
   413  }
   414  
   415  func buildIngressBackendSvc(service string) networkingv1.IngressBackend {
   416  	svcname := strings.Split(service, ":")[0]
   417  	svcport := strings.Split(service, ":")[1]
   418  
   419  	ingressBackend := networkingv1.IngressBackend{
   420  		Service: &networkingv1.IngressServiceBackend{
   421  			Name: svcname,
   422  			Port: parseServiceBackendPort(svcport),
   423  		},
   424  	}
   425  	return ingressBackend
   426  }
   427  
   428  func parseServiceBackendPort(port string) networkingv1.ServiceBackendPort {
   429  	var backendPort networkingv1.ServiceBackendPort
   430  	portIntOrStr := intstr.Parse(port)
   431  
   432  	if portIntOrStr.Type == intstr.Int {
   433  		backendPort.Number = portIntOrStr.IntVal
   434  	}
   435  
   436  	if portIntOrStr.Type == intstr.String {
   437  		backendPort.Name = portIntOrStr.StrVal
   438  	}
   439  	return backendPort
   440  }
   441  
   442  func getIndexHost(host string, rules []networkingv1.IngressRule) int {
   443  	for index, v := range rules {
   444  		if v.Host == host {
   445  			return index
   446  		}
   447  	}
   448  	return -1
   449  }
   450  
   451  func getIndexSecret(secretname string, tls []networkingv1.IngressTLS) int {
   452  	for index, v := range tls {
   453  		if v.SecretName == secretname {
   454  			return index
   455  		}
   456  	}
   457  	return -1
   458  }
   459  

View as plain text