Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SNI match criteria to routes #863

Merged
merged 14 commits into from
Dec 9, 2020
4 changes: 4 additions & 0 deletions deploy/manifests/base/custom-types.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ spec:
- tls
https_redirect_status_code:
type: integer
snis:
type: array
items:
type: string
proxy:
type: object
properties:
Expand Down
6 changes: 5 additions & 1 deletion deploy/single/all-in-one-dbless-k4k8s-enterprise.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ spec:
type: array
regex_priority:
type: integer
snis:
items:
type: string
type: array
strip_path:
type: boolean
upstream:
Expand Down Expand Up @@ -642,7 +646,7 @@ spec:
value: /dev/stderr
- name: KONG_PROXY_ERROR_LOG
value: /dev/stderr
image: kong-docker-kong-enterprise-edition-docker.bintray.io/kong-enterprise-edition:2.2.0.0-alpine
image: kong-docker-kong-enterprise-k8s.bintray.io/kong-enterprise-k8s:2.0.4.1-alpine
lifecycle:
preStop:
exec:
Expand Down
4 changes: 4 additions & 0 deletions deploy/single/all-in-one-dbless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ spec:
type: array
regex_priority:
type: integer
snis:
items:
type: string
type: array
strip_path:
type: boolean
upstream:
Expand Down
6 changes: 5 additions & 1 deletion deploy/single/all-in-one-postgres-enterprise.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ spec:
type: array
regex_priority:
type: integer
snis:
items:
type: string
type: array
strip_path:
type: boolean
upstream:
Expand Down Expand Up @@ -693,7 +697,7 @@ spec:
- name: KONG_STATUS_LISTEN
value: 0.0.0.0:8100
- name: KONG_NGINX_WORKER_PROCESSES
value: "2"
value: "1"
- name: KONG_ADMIN_ACCESS_LOG
value: /dev/stdout
- name: KONG_ADMIN_ERROR_LOG
Expand Down
6 changes: 5 additions & 1 deletion deploy/single/all-in-one-postgres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ spec:
type: array
regex_priority:
type: integer
snis:
items:
type: string
type: array
strip_path:
type: boolean
upstream:
Expand Down Expand Up @@ -655,7 +659,7 @@ spec:
value: /dev/stderr
- name: KONG_PROXY_ERROR_LOG
value: /dev/stderr
image: kong:2.2
image: kong:2.1
lifecycle:
preStop:
exec:
Expand Down
4 changes: 4 additions & 0 deletions hack/dev/common/custom-types.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ spec:
- tls
https_redirect_status_code:
type: integer
snis:
type: array
items:
type: string
proxy:
type: object
properties:
Expand Down
12 changes: 11 additions & 1 deletion internal/ingress/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const (
RegexPriorityKey = "/regex-priority"
HostHeaderKey = "/host-header"
MethodsKey = "/methods"
SNIsKey = "/snis"

// DefaultIngressClass defines the default class used
// by Kong's ingress controller.
Expand Down Expand Up @@ -201,7 +202,16 @@ func ExtractHostHeader(anns map[string]string) string {
func ExtractMethods(anns map[string]string) []string {
val := anns[AnnotationPrefix+MethodsKey]
if val == "" {
return []string{}
return nil
}
return strings.Split(val, ",")
}

// ExtractSNIs extracts the route SNI match criteria annotation value.
func ExtractSNIs(anns map[string]string) ([]string, bool) {
val, exists := anns[AnnotationPrefix+SNIsKey]
if val == "" {
return nil, exists
}
return strings.Split(val, ","), exists
}
35 changes: 34 additions & 1 deletion internal/ingress/annotations/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ func TestExtractMethods(t *testing.T) {
}{
{
name: "empty",
want: []string{},
want: nil,
},
{
name: "non-empty",
Expand All @@ -535,3 +535,36 @@ func TestExtractMethods(t *testing.T) {
})
}
}

func TestExtractSNIs(t *testing.T) {
type args struct {
anns map[string]string
}
tests := []struct {
name string
args args
want []string
}{
{
name: "empty",
want: nil,
},
{
name: "non-empty",
args: args{
anns: map[string]string{
"konghq.com/snis": "hrodna.kong.example,katowice.kong.example",
rainest marked this conversation as resolved.
Show resolved Hide resolved
},
},
want: []string{"hrodna.kong.example", "katowice.kong.example"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got []string
if got, _ = ExtractSNIs(tt.args.anns); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ExtractSNIs() = %v, want %v", got, tt.want)
}
})
}
}
43 changes: 43 additions & 0 deletions internal/ingress/controller/parser/kongstate/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type Route struct {

var validMethods = regexp.MustCompile(`\A[A-Z]+$`)

// hostnames are complicated. shamelessly cribbed from https://stackoverflow.com/a/18494710
// TODO if the Kong core adds support for wildcard SNI route match criteria, this should change
var validSNIs = regexp.MustCompile(`^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$`)

// normalizeProtocols prevents users from mismatching grpc/http
func (r *Route) normalizeProtocols() {
protocols := r.Protocols
Expand Down Expand Up @@ -184,6 +188,30 @@ func (r *Route) overrideMethods(log logrus.FieldLogger, anns map[string]string)
r.Methods = methods
}

func (r *Route) overrideSNIs(log logrus.FieldLogger, anns map[string]string) {
var annSNIs []string
var exists bool
annSNIs, exists = annotations.ExtractSNIs(anns)
// this is not a length check because we specifically want to provide a means
// to set "no SNI criteria", by providing the annotation with an empty string value
rainest marked this conversation as resolved.
Show resolved Hide resolved
if !exists {
return
}
var snis []*string
for _, sni := range annSNIs {
sanitizedSNI := strings.TrimSpace(sni)
if validSNIs.MatchString(sanitizedSNI) {
snis = append(snis, kong.String(sanitizedSNI))
} else {
// SNI is not a valid hostname
log.WithField("kongroute", r.Name).Errorf("invalid SNI: %v", sni)
return
}
}

r.SNIs = snis
}

// overrideByAnnotation sets Route protocols via annotation
func (r *Route) overrideByAnnotation(log logrus.FieldLogger) {
r.overrideProtocols(r.Ingress.Annotations)
Expand All @@ -192,6 +220,7 @@ func (r *Route) overrideByAnnotation(log logrus.FieldLogger) {
r.overridePreserveHost(r.Ingress.Annotations)
r.overrideRegexPriority(r.Ingress.Annotations)
r.overrideMethods(log, r.Ingress.Annotations)
r.overrideSNIs(log, r.Ingress.Annotations)
}

// override sets Route fields by KongIngress first, then by annotation
Expand Down Expand Up @@ -260,4 +289,18 @@ func (r *Route) overrideByKongIngress(log logrus.FieldLogger, kongIngress *confi
if ir.PathHandling != nil {
r.PathHandling = kong.String(*ir.PathHandling)
}
if len(ir.SNIs) != 0 {
var SNIs []*string
for _, unsanitizedSNI := range ir.SNIs {
SNI := strings.TrimSpace(*unsanitizedSNI)
if validSNIs.MatchString(SNI) {
SNIs = append(SNIs, kong.String(SNI))
} else {
// SNI is not a valid hostname
log.WithField("kongroute", ir.Name).Errorf("invalid SNI: %v", unsanitizedSNI)
return
}
}
r.SNIs = SNIs
}
}
51 changes: 51 additions & 0 deletions internal/ingress/controller/parser/kongstate/route_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,3 +771,54 @@ func Test_overrideRouteMethods(t *testing.T) {
})
}
}

func Test_overrideRouteSNIs(t *testing.T) {
type args struct {
route Route
anns map[string]string
}
tests := []struct {
name string
args args
want Route
}{
{name: "basic empty route"},
{
name: "basic sanity, with strippable space",
args: args{
anns: map[string]string{
"konghq.com/snis": "hrodna.kong.example, katowice.kong.example",
},
},
want: Route{
Route: kong.Route{
SNIs: kong.StringSlice("hrodna.kong.example", "katowice.kong.example"),
},
},
},
{
name: "not hostnames at all",
args: args{
anns: map[string]string{
"konghq.com/snis": "-10,GET",
},
},
},
{
name: "wildcard hostname, not valid for SNI",
args: args{
anns: map[string]string{
"konghq.com/snis": "*.example.com",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.route.overrideSNIs(logrus.New(), tt.args.anns)
if !reflect.DeepEqual(tt.args.route, tt.want) {
t.Errorf("overrideRouteSNIs() got = %v, want %v", tt.args.route, tt.want)
}
})
}
}
Loading