/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package create import ( "context" "fmt" "regexp" "strings" "github.com/spf13/cobra" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" networkingv1client "k8s.io/client-go/kubernetes/typed/networking/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) var ( // Explaining the Regex below: // ^(?P[\w\*\-\.]*) -> Indicates the host - 0-N characters of letters, number, underscore, '-', '.' and '*' // (?P/.*) -> Indicates the path and MUST start with '/' - / + 0-N characters // Separator from host/path to svcname:svcport -> "=" // (?P[\w\-]+) -> Service Name (letters, numbers, '-') -> 1-N characters // Separator from svcname to svcport -> ":" // (?P[\w\-]+) -> Service Port (letters, numbers, '-') -> 1-N characters regexHostPathSvc = `^(?P[\w\*\-\.]*)(?P/.*)=(?P[\w\-]+):(?P[\w\-]+)` // This Regex is optional -> (....)? // (?Ptls) -> Verify if the argument after "," is 'tls' // Optional Separator from tls to the secret name -> "=?" // (?P[\w\-]+)? -> Optional secret name after the separator -> 1-N characters regexTLS = `(,(?Ptls)=?(?P[\w\-]+)?)?` // The validation Regex is the concatenation of hostPathSvc validation regex // and the TLS validation regex ruleRegex = regexHostPathSvc + regexTLS ingressLong = templates.LongDesc(i18n.T(` Create an ingress with the specified name.`)) ingressExample = templates.Examples(i18n.T(` # Create a single ingress called 'simple' that directs requests to foo.com/bar to svc # svc1:8080 with a tls secret "my-cert" kubectl create ingress simple --rule="foo.com/bar=svc1:8080,tls=my-cert" # Create a catch all ingress of "/path" pointing to service svc:port and Ingress Class as "otheringress" kubectl create ingress catch-all --class=otheringress --rule="/path=svc:port" # Create an ingress with two annotations: ingress.annotation1 and ingress.annotations2 kubectl create ingress annotated --class=default --rule="foo.com/bar=svc:port" \ --annotation ingress.annotation1=foo \ --annotation ingress.annotation2=bla # Create an ingress with the same host and multiple paths kubectl create ingress multipath --class=default \ --rule="foo.com/=svc:port" \ --rule="foo.com/admin/=svcadmin:portadmin" # Create an ingress with multiple hosts and the pathType as Prefix kubectl create ingress ingress1 --class=default \ --rule="foo.com/path*=svc:8080" \ --rule="bar.com/admin*=svc2:http" # Create an ingress with TLS enabled using the default ingress certificate and different path types kubectl create ingress ingtls --class=default \ --rule="foo.com/=svc:https,tls" \ --rule="foo.com/path/subpath*=othersvc:8080" # Create an ingress with TLS enabled using a specific secret and pathType as Prefix kubectl create ingress ingsecret --class=default \ --rule="foo.com/*=svc:8080,tls=secret1" # Create an ingress with a default backend kubectl create ingress ingdefault --class=default \ --default-backend=defaultsvc:http \ --rule="foo.com/*=svc:8080,tls=secret1" `)) ) // CreateIngressOptions is returned by NewCmdCreateIngress type CreateIngressOptions struct { PrintFlags *genericclioptions.PrintFlags PrintObj func(obj runtime.Object) error Name string IngressClass string Rules []string Annotations []string DefaultBackend string Namespace string EnforceNamespace bool CreateAnnotation bool Client networkingv1client.NetworkingV1Interface DryRunStrategy cmdutil.DryRunStrategy DryRunVerifier *resource.DryRunVerifier FieldManager string genericclioptions.IOStreams } // NewCreateIngressOptions creates the CreateIngressOptions to be used later func NewCreateIngressOptions(ioStreams genericclioptions.IOStreams) *CreateIngressOptions { return &CreateIngressOptions{ PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), IOStreams: ioStreams, } } // NewCmdCreateIngress is a macro command to create a new ingress. // This command is better known to users as `kubectl create ingress`. func NewCmdCreateIngress(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { o := NewCreateIngressOptions(ioStreams) cmd := &cobra.Command{ Use: "ingress NAME --rule=host/path=service:port[,tls[=secret]] ", DisableFlagsInUseLine: true, Aliases: []string{"ing"}, Short: ingressLong, Long: ingressLong, Example: ingressExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.Run()) }, } o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddDryRunFlag(cmd) cmd.Flags().StringVar(&o.IngressClass, "class", o.IngressClass, "Ingress Class to be used") 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.") cmd.Flags().StringVar(&o.DefaultBackend, "default-backend", o.DefaultBackend, "Default service for backend, in format of svcname:port") cmd.Flags().StringArrayVar(&o.Annotations, "annotation", o.Annotations, "Annotation to insert in the ingress object, in the format annotation=value") cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") return cmd } // Complete completes all the options func (o *CreateIngressOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { name, err := NameFromCommandArgs(cmd, args) if err != nil { return err } o.Name = name clientConfig, err := f.ToRESTConfig() if err != nil { return err } o.Client, err = networkingv1client.NewForConfig(clientConfig) if err != nil { return err } o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { return err } dynamicClient, err := f.DynamicClient() if err != nil { return err } o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, f.OpenAPIGetter()) cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) printer, err := o.PrintFlags.ToPrinter() if err != nil { return err } o.PrintObj = func(obj runtime.Object) error { return printer.PrintObj(obj, o.Out) } return nil } // Validate validates the Ingress object to be created func (o *CreateIngressOptions) Validate() error { if len(o.DefaultBackend) == 0 && len(o.Rules) == 0 { 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)") } rulevalidation, err := regexp.Compile(ruleRegex) if err != nil { return fmt.Errorf("failed to compile the regex") } for _, rule := range o.Rules { if match := rulevalidation.MatchString(rule); !match { return fmt.Errorf("rule %s is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", rule) } } if len(o.DefaultBackend) > 0 && len(strings.Split(o.DefaultBackend, ":")) != 2 { return fmt.Errorf("default-backend should be in format servicename:serviceport") } return nil } // Run performs the execution of 'create ingress' sub command func (o *CreateIngressOptions) Run() error { ingress := o.createIngress() if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, ingress, scheme.DefaultJSONEncoder()); err != nil { return err } if o.DryRunStrategy != cmdutil.DryRunClient { createOptions := metav1.CreateOptions{} if o.FieldManager != "" { createOptions.FieldManager = o.FieldManager } if o.DryRunStrategy == cmdutil.DryRunServer { if err := o.DryRunVerifier.HasSupport(ingress.GroupVersionKind()); err != nil { return err } createOptions.DryRun = []string{metav1.DryRunAll} } var err error ingress, err = o.Client.Ingresses(o.Namespace).Create(context.TODO(), ingress, createOptions) if err != nil { return fmt.Errorf("failed to create ingress: %v", err) } } return o.PrintObj(ingress) } func (o *CreateIngressOptions) createIngress() *networkingv1.Ingress { namespace := "" if o.EnforceNamespace { namespace = o.Namespace } annotations := o.buildAnnotations() spec := o.buildIngressSpec() ingress := &networkingv1.Ingress{ TypeMeta: metav1.TypeMeta{APIVersion: networkingv1.SchemeGroupVersion.String(), Kind: "Ingress"}, ObjectMeta: metav1.ObjectMeta{ Name: o.Name, Namespace: namespace, Annotations: annotations, }, Spec: spec, } return ingress } func (o *CreateIngressOptions) buildAnnotations() map[string]string { var annotations map[string]string annotations = make(map[string]string) for _, annotation := range o.Annotations { an := strings.SplitN(annotation, "=", 2) annotations[an[0]] = an[1] } return annotations } // buildIngressSpec builds the .spec from the diverse arguments passed to kubectl func (o *CreateIngressOptions) buildIngressSpec() networkingv1.IngressSpec { var ingressSpec networkingv1.IngressSpec if len(o.IngressClass) > 0 { ingressSpec.IngressClassName = &o.IngressClass } if len(o.DefaultBackend) > 0 { defaultbackend := buildIngressBackendSvc(o.DefaultBackend) ingressSpec.DefaultBackend = &defaultbackend } ingressSpec.TLS = o.buildTLSRules() ingressSpec.Rules = o.buildIngressRules() return ingressSpec } func (o *CreateIngressOptions) buildTLSRules() []networkingv1.IngressTLS { var hostAlreadyPresent map[string]struct{} hostAlreadyPresent = make(map[string]struct{}) ingressTLSs := []networkingv1.IngressTLS{} var secret string for _, rule := range o.Rules { tls := strings.Split(rule, ",") if len(tls) == 2 { ingressTLS := networkingv1.IngressTLS{} host := strings.SplitN(rule, "/", 2)[0] secret = "" secretName := strings.Split(tls[1], "=") if len(secretName) > 1 { secret = secretName[1] } idxSecret := getIndexSecret(secret, ingressTLSs) // We accept the same host into TLS secrets only once if _, ok := hostAlreadyPresent[host]; !ok { if idxSecret > -1 { ingressTLSs[idxSecret].Hosts = append(ingressTLSs[idxSecret].Hosts, host) hostAlreadyPresent[host] = struct{}{} continue } if host != "" { ingressTLS.Hosts = append(ingressTLS.Hosts, host) } if secret != "" { ingressTLS.SecretName = secret } if len(ingressTLS.SecretName) > 0 || len(ingressTLS.Hosts) > 0 { ingressTLSs = append(ingressTLSs, ingressTLS) } hostAlreadyPresent[host] = struct{}{} } } } return ingressTLSs } // buildIngressRules builds the .spec.rules for an ingress object. func (o *CreateIngressOptions) buildIngressRules() []networkingv1.IngressRule { ingressRules := []networkingv1.IngressRule{} for _, rule := range o.Rules { removeTLS := strings.Split(rule, ",")[0] hostSplit := strings.SplitN(removeTLS, "/", 2) host := hostSplit[0] ingressPath := buildHTTPIngressPath(hostSplit[1]) ingressRule := networkingv1.IngressRule{} if host != "" { ingressRule.Host = host } idxHost := getIndexHost(ingressRule.Host, ingressRules) if idxHost > -1 { ingressRules[idxHost].IngressRuleValue.HTTP.Paths = append(ingressRules[idxHost].IngressRuleValue.HTTP.Paths, ingressPath) continue } ingressRule.IngressRuleValue = networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ ingressPath, }, }, } ingressRules = append(ingressRules, ingressRule) } return ingressRules } func buildHTTPIngressPath(pathsvc string) networkingv1.HTTPIngressPath { pathsvcsplit := strings.Split(pathsvc, "=") path := "/" + pathsvcsplit[0] service := pathsvcsplit[1] var pathType networkingv1.PathType pathType = "Exact" // If * in the End, turn pathType=Prefix but remove the * from the end if path[len(path)-1:] == "*" { pathType = "Prefix" path = path[0 : len(path)-1] } httpIngressPath := networkingv1.HTTPIngressPath{ Path: path, PathType: &pathType, Backend: buildIngressBackendSvc(service), } return httpIngressPath } func buildIngressBackendSvc(service string) networkingv1.IngressBackend { svcname := strings.Split(service, ":")[0] svcport := strings.Split(service, ":")[1] ingressBackend := networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: svcname, Port: parseServiceBackendPort(svcport), }, } return ingressBackend } func parseServiceBackendPort(port string) networkingv1.ServiceBackendPort { var backendPort networkingv1.ServiceBackendPort portIntOrStr := intstr.Parse(port) if portIntOrStr.Type == intstr.Int { backendPort.Number = portIntOrStr.IntVal } if portIntOrStr.Type == intstr.String { backendPort.Name = portIntOrStr.StrVal } return backendPort } func getIndexHost(host string, rules []networkingv1.IngressRule) int { for index, v := range rules { if v.Host == host { return index } } return -1 } func getIndexSecret(secretname string, tls []networkingv1.IngressTLS) int { for index, v := range tls { if v.SecretName == secretname { return index } } return -1 }