From 73aa0a92f8c79d18a53dd39d3064ac3b0257f11b Mon Sep 17 00:00:00 2001 From: Ricardo Pchevuzinske Katz Date: Tue, 13 Oct 2020 15:22:00 -0300 Subject: [PATCH] Add support for create ingress in kubectl Signed-off-by: Ricardo Pchevuzinske Katz --- .../src/k8s.io/kubectl/pkg/cmd/create/BUILD | 1 + .../kubectl/pkg/cmd/create/create_ingress.go | 370 +++++++++++--- .../pkg/cmd/create/create_ingress_test.go | 464 +++++++++++++++--- 3 files changed, 693 insertions(+), 142 deletions(-) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD b/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD index 3c8812c6417..54b2e7e27bb 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD @@ -37,6 +37,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/intstr:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress.go b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress.go index 311875aa58d..beaab24d482 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress.go @@ -19,29 +19,86 @@ package create import ( "context" "fmt" - "strconv" + "regexp" + "strings" "github.com/spf13/cobra" - "k8s.io/api/networking/v1" + 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.+) -> Indicates the host - 1-N characters + // (?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.+)(?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.`)) + Create an ingress with the specified name.`)) ingressExample = templates.Examples(i18n.T(` - # Create a new ingress named my-app. - kubectl create ingress my-app --host=foo.bar.com --service-name=my-svc`)) + # 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 pointing to service svc:port and Ingress Class as "otheringress" + kubectl create ingress catch-all --class=otheringress --rule="_/=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 @@ -50,24 +107,26 @@ type CreateIngressOptions struct { PrintObj func(obj runtime.Object) error - Name string - Host string - ServiceName string - ServicePort string - Path string + Name string + IngressClass string + Rules []string + Annotations []string + DefaultBackend string + Namespace string + EnforceNamespace bool + CreateAnnotation bool - Namespace string - Client *networkingv1client.NetworkingV1Client + Client networkingv1client.NetworkingV1Interface DryRunStrategy cmdutil.DryRunStrategy DryRunVerifier *resource.DryRunVerifier - Builder *resource.Builder - Cmd *cobra.Command + + FieldManager string genericclioptions.IOStreams } -// NewCreateCreateIngressOptions creates and returns an instance of CreateIngressOptions -func NewCreateCreateIngressOptions(ioStreams genericclioptions.IOStreams) *CreateIngressOptions { +// 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, @@ -75,15 +134,17 @@ func NewCreateCreateIngressOptions(ioStreams genericclioptions.IOStreams) *Creat } // 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 := NewCreateCreateIngressOptions(ioStreams) + o := NewCreateIngressOptions(ioStreams) cmd := &cobra.Command{ - Use: "ingress NAME --host=hostname| --service-name=servicename [--service-port=serviceport] [--path=path] [--dry-run]", - Aliases: []string{"ing"}, - Short: i18n.T("Create an ingress with the specified name."), - Long: ingressLong, - Example: ingressExample, + 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()) @@ -95,13 +156,12 @@ func NewCmdCreateIngress(f cmdutil.Factory, ioStreams genericclioptions.IOStream cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) - cmdutil.AddDryRunFlag(cmd) - cmd.Flags().StringVar(&o.Host, "host", o.Host, i18n.T("Host name this Ingress should route traffic on")) - cmd.Flags().StringVar(&o.ServiceName, "service-name", o.ServiceName, i18n.T("Service this Ingress should route traffic to")) - cmd.Flags().StringVar(&o.ServicePort, "service-port", o.ServicePort, "Port name or number of the Service to route traffic to") - cmd.Flags().StringVar(&o.Path, "path", o.Path, "Path on which to route traffic to") - cmd.MarkFlagRequired("host") - cmd.MarkFlagRequired("service-name") + 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 } @@ -122,12 +182,12 @@ func (o *CreateIngressOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, a return err } - o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() + o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { return err } - o.Builder = f.NewBuilder() - o.Cmd = cmd + + o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) if err != nil { @@ -143,6 +203,7 @@ func (o *CreateIngressOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, a } o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient) cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) + printer, err := o.PrintFlags.ToPrinter() if err != nil { return err @@ -150,21 +211,46 @@ func (o *CreateIngressOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, a 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 { - var ingress *v1.Ingress - ingress = o.createIngress() + 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 @@ -177,47 +263,199 @@ func (o *CreateIngressOptions) Run() error { return fmt.Errorf("failed to create ingress: %v", err) } } - return o.PrintObj(ingress) } -func (o *CreateIngressOptions) createIngress() *v1.Ingress { - i := &v1.Ingress{ - TypeMeta: metav1.TypeMeta{APIVersion: v1.SchemeGroupVersion.String(), Kind: "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, + Name: o.Name, + Namespace: namespace, + Annotations: annotations, }, - Spec: v1.IngressSpec{ - Rules: []v1.IngressRule{ - { - Host: o.Host, - IngressRuleValue: v1.IngressRuleValue{ - HTTP: &v1.HTTPIngressRuleValue{ - Paths: []v1.HTTPIngressPath{ - { - Path: o.Path, - Backend: v1.IngressBackend{ - Service: &v1.IngressServiceBackend{ - Name: o.ServiceName, - }, - }, - }, - }, - }, - }, + 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 +} - var port v1.ServiceBackendPort - if n, err := strconv.Atoi(o.ServicePort); err != nil { - port.Name = o.ServicePort - } else { - port.Number = int32(n) +func parseServiceBackendPort(port string) networkingv1.ServiceBackendPort { + var backendPort networkingv1.ServiceBackendPort + portIntOrStr := intstr.Parse(port) + + if portIntOrStr.Type == intstr.Int { + backendPort.Number = portIntOrStr.IntVal } - i.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Backend.Service.Port = port - - return i + 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 } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress_test.go index c37b54dfd27..b7f3f67086e 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_ingress_test.go @@ -17,49 +17,327 @@ limitations under the License. package create import ( - "strings" "testing" - "k8s.io/api/networking/v1" + networkingv1 "k8s.io/api/networking/v1" + v1 "k8s.io/api/networking/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestCreateIngress(t *testing.T) { - ingressName := "fake-ingress" +func TestCreateIngressValidation(t *testing.T) { tests := map[string]struct { - name string - host string - serviceName string - servicePort string - path string - expectErrMsg string - expect *v1.Ingress + defaultbackend string + ingressclass string + rules []string + expected string }{ - "test-valid-case": { - name: "fake-ingress", - host: "foo.bar.com", - serviceName: "fake-service", - servicePort: "https", - path: "/api", - expect: &v1.Ingress{ - TypeMeta: metav1.TypeMeta{APIVersion: v1.SchemeGroupVersion.String(), Kind: "Ingress"}, - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-ingress", + "no default backend and rule": { + defaultbackend: "", + rules: []string{}, + expected: "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)", + }, + "invalid default backend separator": { + defaultbackend: "xpto,4444", + expected: "default-backend should be in format servicename:serviceport", + }, + "default backend without port": { + defaultbackend: "xpto", + expected: "default-backend should be in format servicename:serviceport", + }, + "default backend is ok": { + defaultbackend: "xpto:4444", + expected: "", + }, + "multiple conformant rules": { + rules: []string{ + "foo.com/path*=svc:8080", + "bar.com/admin*=svc2:http", + }, + expected: "", + }, + "one invalid and two valid rules": { + rules: []string{ + "foo.com=svc:redis,tls", + "foo.com/path/subpath*=othersvc:8080", + "foo.com/*=svc:8080,tls=secret1", + }, + expected: "rule foo.com=svc:redis,tls is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", + }, + "service without port": { + rules: []string{ + "foo.com/=svc,tls", + }, + expected: "rule foo.com/=svc,tls is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", + }, + "valid tls rule without secret": { + rules: []string{ + "foo.com/=svc:http,tls=", + }, + expected: "", + }, + "valid tls rule with secret": { + rules: []string{ + "foo.com/=svc:http,tls=secret123", + }, + expected: "", + }, + "valid path with type prefix": { + rules: []string{ + "foo.com/admin*=svc:8080", + }, + expected: "", + }, + "wildcard host": { + rules: []string{ + "*.foo.com/admin*=svc:8080", + }, + expected: "", + }, + "invalid separation between ingress and service": { + rules: []string{ + "*.foo.com/path,svc:8080", + }, + expected: "rule *.foo.com/path,svc:8080 is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", + }, + "two invalid and one valid rule": { + rules: []string{ + "foo.com/path/subpath*=svc:redis,tls=blo", + "foo.com=othersvc:8080", + "foo.com/admin=svc,tls=secret1", + }, + expected: "rule foo.com=othersvc:8080 is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + o := &CreateIngressOptions{ + DefaultBackend: tc.defaultbackend, + Rules: tc.rules, + IngressClass: tc.ingressclass, + } + + err := o.Validate() + if err != nil && err.Error() != tc.expected { + t.Errorf("unexpected error: %v", err) + } + if tc.expected != "" && err == nil { + t.Errorf("expected error, got no error") + } + + }) + } +} + +func TestCreateIngress(t *testing.T) { + ingressName := "test-ingress" + ingressClass := "nginx" + pathTypeExact := networkingv1.PathTypeExact + pathTypePrefix := networkingv1.PathTypePrefix + tests := map[string]struct { + defaultbackend string + rules []string + ingressclass string + annotations []string + expected *networkingv1.Ingress + }{ + "catch all host and default backend with default TLS returns empty TLS": { + rules: []string{ + "_/=catchall:8080,tls=", + }, + ingressclass: ingressClass, + defaultbackend: "service1:https", + annotations: []string{}, + expected: &networkingv1.Ingress{ + TypeMeta: metav1.TypeMeta{ + APIVersion: networkingv1.SchemeGroupVersion.String(), + Kind: "Ingress", }, - Spec: v1.IngressSpec{ - Rules: []v1.IngressRule{ + ObjectMeta: metav1.ObjectMeta{ + Name: ingressName, + Annotations: map[string]string{}, + }, + Spec: networkingv1.IngressSpec{ + IngressClassName: &ingressClass, + DefaultBackend: &networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "service1", + Port: networkingv1.ServiceBackendPort{ + Name: "https", + }, + }, + }, + TLS: []v1.IngressTLS{}, + Rules: []networkingv1.IngressRule{ { - Host: "foo.bar.com", - IngressRuleValue: v1.IngressRuleValue{ - HTTP: &v1.HTTPIngressRuleValue{ - Paths: []v1.HTTPIngressPath{ + Host: "", + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ { - Path: "/api", - Backend: v1.IngressBackend{ - Service: &v1.IngressServiceBackend{ - Name: "fake-service", - Port: v1.ServiceBackendPort{ + Path: "/", + PathType: &pathTypeExact, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "catchall", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "mixed hosts with mixed TLS configuration and a default backend": { + rules: []string{ + "foo.com/=foo-svc:8080,tls=", + "foo.com/admin=foo-admin-svc:http,tls=", + "bar.com/prefix*=bar-svc:8080,tls=bar-secret", + "bar.com/noprefix=barnp-svc:8443,tls", + "foobar.com/*=foobar-svc:https", + "foobar1.com/*=foobar1-svc:https,tls=bar-secret", + }, + defaultbackend: "service2:8080", + annotations: []string{}, + expected: &networkingv1.Ingress{ + TypeMeta: metav1.TypeMeta{ + APIVersion: networkingv1.SchemeGroupVersion.String(), + Kind: "Ingress", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ingressName, + Annotations: map[string]string{}, + }, + Spec: networkingv1.IngressSpec{ + DefaultBackend: &networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "service2", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + TLS: []v1.IngressTLS{ + { + Hosts: []string{ + "foo.com", + }, + }, + { + Hosts: []string{ + "bar.com", + "foobar1.com", + }, + SecretName: "bar-secret", + }, + }, + Rules: []networkingv1.IngressRule{ + { + Host: "foo.com", + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathTypeExact, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "foo-svc", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + { + Path: "/admin", + PathType: &pathTypeExact, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "foo-admin-svc", + Port: networkingv1.ServiceBackendPort{ + Name: "http", + }, + }, + }, + }, + }, + }, + }, + }, + { + Host: "bar.com", + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/prefix", + PathType: &pathTypePrefix, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "bar-svc", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + { + Path: "/noprefix", + PathType: &pathTypeExact, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "barnp-svc", + Port: networkingv1.ServiceBackendPort{ + Number: 8443, + }, + }, + }, + }, + }, + }, + }, + }, + { + Host: "foobar.com", + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathTypePrefix, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "foobar-svc", + Port: networkingv1.ServiceBackendPort{ + Name: "https", + }, + }, + }, + }, + }, + }, + }, + }, + { + Host: "foobar1.com", + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathTypePrefix, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "foobar1-svc", + Port: networkingv1.ServiceBackendPort{ Name: "https", }, }, @@ -73,56 +351,90 @@ func TestCreateIngress(t *testing.T) { }, }, }, + "simple ingress with annotation": { + rules: []string{ + "foo.com/=svc:8080,tls=secret1", + "foo.com/subpath*=othersvc:8080,tls=secret1", + }, + annotations: []string{ + "ingress.kubernetes.io/annotation1=bla", + "ingress.kubernetes.io/annotation2=blo", + "ingress.kubernetes.io/annotation3=ble", + }, + expected: &networkingv1.Ingress{ + TypeMeta: metav1.TypeMeta{ + APIVersion: networkingv1.SchemeGroupVersion.String(), + Kind: "Ingress", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ingressName, + Annotations: map[string]string{ + "ingress.kubernetes.io/annotation1": "bla", + "ingress.kubernetes.io/annotation3": "ble", + "ingress.kubernetes.io/annotation2": "blo", + }, + }, + Spec: networkingv1.IngressSpec{ + TLS: []v1.IngressTLS{ + { + Hosts: []string{ + "foo.com", + }, + SecretName: "secret1", + }, + }, + Rules: []networkingv1.IngressRule{ + { + Host: "foo.com", + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathTypeExact, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "svc", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + { + Path: "/subpath", + PathType: &pathTypePrefix, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "othersvc", + Port: networkingv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, } + for name, tc := range tests { t.Run(name, func(t *testing.T) { o := &CreateIngressOptions{ - Name: ingressName, - Host: tc.host, - ServiceName: tc.serviceName, - ServicePort: tc.servicePort, - Path: tc.path, + Name: ingressName, + IngressClass: tc.ingressclass, + Annotations: tc.annotations, + DefaultBackend: tc.defaultbackend, + Rules: tc.rules, } ingress := o.createIngress() - if !apiequality.Semantic.DeepEqual(ingress, tc.expect) { - t.Errorf("expected:\n%+v\ngot:\n%+v", tc.expect, ingress) - } - }) - } - -} - -func TestCreateIngressValidation(t *testing.T) { - tests := map[string]struct { - name string - host string - serviceName string - servicePort string - path string - expect string - }{ - "test-missing-host": { - serviceName: "fake-ingress", - expect: "--host must be specified", - }, - "test-missing-service": { - host: "foo.bar.com", - expect: "--service-name must be specified", - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - o := &CreateIngressOptions{ - Host: tc.host, - ServiceName: tc.serviceName, - ServicePort: tc.servicePort, - Path: tc.path, - } - - err := o.Validate() - if err != nil && !strings.Contains(err.Error(), tc.expect) { - t.Errorf("unexpected error: %v", err) + if !apiequality.Semantic.DeepEqual(ingress, tc.expected) { + t.Errorf("expected:\n%#v\ngot:\n%#v", tc.expected, ingress) } }) }