mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 12:15:52 +00:00
Add support for create ingress in kubectl
Signed-off-by: Ricardo Pchevuzinske Katz <ricardo.katz@gmail.com>
This commit is contained in:
parent
46b5eb3338
commit
73aa0a92f8
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user