Add support for create ingress in kubectl

Signed-off-by: Ricardo Pchevuzinske Katz <ricardo.katz@gmail.com>
This commit is contained in:
Ricardo Pchevuzinske Katz 2020-10-13 15:22:00 -03:00
parent 46b5eb3338
commit 73aa0a92f8
3 changed files with 693 additions and 142 deletions

View File

@ -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",

View File

@ -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<host>.+) -> Indicates the host - 1-N characters
// (?P<path>/.*) -> Indicates the path and MUST start with '/' - / + 0-N characters
// Separator from host/path to svcname:svcport -> "="
// (?P<svcname>[\w\-]+) -> Service Name (letters, numbers, '-') -> 1-N characters
// Separator from svcname to svcport -> ":"
// (?P<svcport>[\w\-]+) -> Service Port (letters, numbers, '-') -> 1-N characters
regexHostPathSvc = `^(?P<host>.+)(?P<path>/.*)=(?P<svcname>[\w\-]+):(?P<svcport>[\w\-]+)`
// This Regex is optional -> (....)?
// (?P<istls>tls) -> Verify if the argument after "," is 'tls'
// Optional Separator from tls to the secret name -> "=?"
// (?P<secretname>[\w\-]+)? -> Optional secret name after the separator -> 1-N characters
regexTLS = `(,(?P<istls>tls)=?(?P<secretname>[\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
}

View File

@ -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)
}
})
}