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 missing Service/Route annotations #3121

Merged
merged 6 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ Adding a new version? You'll need three changes:
irrelevant secrets (e.g: service account tokens) are not stored. This change
is made to reduce memory usage of the cache.
[#3047](https://github.com/Kong/kubernetes-ingress-controller/pull/3047)
- Services support annotations for connect, read, and write timeouts.
[#3121](https://github.com/Kong/kubernetes-ingress-controller/pull/3121)
- Services support annotations for retries.
[#3121](https://github.com/Kong/kubernetes-ingress-controller/pull/3121)
- Routes support annotations for headers.
[#3121](https://github.com/Kong/kubernetes-ingress-controller/pull/3121)
- Routes support annotations for path handling.
[#3121](https://github.com/Kong/kubernetes-ingress-controller/pull/3121)

### Fixed

Expand Down
70 changes: 70 additions & 0 deletions internal/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ const (
ResponseBuffering = "/response-buffering"
HostAliasesKey = "/host-aliases"
RegexPrefixKey = "/regex-prefix"
ConnectTimeoutKey = "/connect-timeout"
WriteTimeoutKey = "/write-timeout"
ReadTimeoutKey = "/read-timeout"
RetriesKey = "/retries"
HeadersKey = "/headers"
PathHandlingKey = "/path-handling"

// GatewayClassUnmanagedAnnotationSuffix is an annotation used on a Gateway resource to
// indicate that the GatewayClass should be reconciled according to unmanaged
Expand Down Expand Up @@ -253,6 +259,70 @@ func ExtractHostAliases(anns map[string]string) ([]string, bool) {
return strings.Split(val, ","), true
}

// ExtractConnectTimeout extracts the connection timeout annotation value.
func ExtractConnectTimeout(anns map[string]string) (string, bool) {
val, exists := anns[AnnotationPrefix+ConnectTimeoutKey]
if !exists {
return "", false
}
return val, true
}

// ExtractWriteTimeout extracts the write timeout annotation value.
func ExtractWriteTimeout(anns map[string]string) (string, bool) {
val, exists := anns[AnnotationPrefix+WriteTimeoutKey]
if !exists {
return "", false
}
return val, true
}

// ExtractReadTimeout extracts the read timeout annotation value.
func ExtractReadTimeout(anns map[string]string) (string, bool) {
val, exists := anns[AnnotationPrefix+ReadTimeoutKey]
if !exists {
return "", false
}
return val, true
}

// ExtractRetries extracts the retries annotation value.
func ExtractRetries(anns map[string]string) (string, bool) {
val, exists := anns[AnnotationPrefix+RetriesKey]
if !exists {
return "", false
}
return val, true
}

// ExtractHeaders extracts the parsed headers annotations values. It returns a map of header names to slices of values.
func ExtractHeaders(anns map[string]string) (map[string][]string, bool) {
headers := make(map[string][]string)
prefix := AnnotationPrefix + HeadersKey + "/"
for key, val := range anns {
if strings.HasPrefix(key, prefix) {
header := strings.TrimPrefix(key, prefix)
if len(header) == 0 || len(val) == 0 {
continue
}
headers[header] = strings.Split(val, ",")
}
}
if len(headers) == 0 {
return headers, false
}
return headers, true
}

// ExtractPathHandling extracts the path handling annotation value.
func ExtractPathHandling(anns map[string]string) (string, bool) {
val, exists := anns[AnnotationPrefix+PathHandlingKey]
if !exists {
return "", false
}
return val, true
}

// ExtractUnmanagedGatewayClassMode extracts the value of the unmanaged gateway
// mode annotation.
func ExtractUnmanagedGatewayClassMode(anns map[string]string) string {
Expand Down
243 changes: 243 additions & 0 deletions internal/annotations/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1"
netv1 "k8s.io/api/networking/v1"
Expand Down Expand Up @@ -684,3 +685,245 @@ func TestExtractHostAliases(t *testing.T) {
})
}
}

func TestExtractConnectTimeout(t *testing.T) {
type args struct {
anns map[string]string
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
want: "",
},
{
name: "non-empty",
args: args{
anns: map[string]string{
"konghq.com/connect-timeout": "3000",
},
},
want: "3000",
},
}
for _, tt := range tests {
randmonkey marked this conversation as resolved.
Show resolved Hide resolved
t.Run(tt.name, func(t *testing.T) {
got, ok := ExtractConnectTimeout(tt.args.anns)
if tt.want == "" {
assert.False(t, ok)
} else {
assert.True(t, ok)
require.Equal(t, tt.want, got)
}
})
}
}

func TestExtractWriteTimeout(t *testing.T) {
type args struct {
anns map[string]string
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
want: "",
},
{
name: "non-empty",
args: args{
anns: map[string]string{
"konghq.com/write-timeout": "3000",
},
},
want: "3000",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rainest marked this conversation as resolved.
Show resolved Hide resolved
got, ok := ExtractWriteTimeout(tt.args.anns)
if tt.want == "" {
assert.False(t, ok)
} else {
assert.True(t, ok)
require.Equal(t, tt.want, got)
}
})
}
}

func TestExtractReadTimeout(t *testing.T) {
type args struct {
anns map[string]string
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
want: "",
},
{
name: "non-empty",
args: args{
anns: map[string]string{
"konghq.com/read-timeout": "3000",
},
},
want: "3000",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := ExtractReadTimeout(tt.args.anns)
if tt.want == "" {
assert.False(t, ok)
} else {
assert.True(t, ok)
require.Equal(t, tt.want, got)
}
})
}
}

func TestExtractRetries(t *testing.T) {
type args struct {
anns map[string]string
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
want: "",
},
{
name: "non-empty",
args: args{
anns: map[string]string{
"konghq.com/retries": "3000",
},
},
want: "3000",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := ExtractRetries(tt.args.anns)
if tt.want == "" {
assert.False(t, ok)
} else {
assert.True(t, ok)
}
if got != tt.want {
t.Errorf("ExtractRetries() = %v, want %v", got, tt.want)
}
})
}
}

func TestExtractHeaders(t *testing.T) {
type args struct {
anns map[string]string
}
tests := []struct {
name string
args args
want map[string][]string
}{
{
name: "empty",
want: map[string][]string{},
},
{
name: "non-empty",
args: args{
anns: map[string]string{
"konghq.com/headers/foo": "foo",
},
},
want: map[string][]string{"foo": {"foo"}},
},
{
name: "no separator",
args: args{
anns: map[string]string{
"konghq.com/headersfoo": "foo",
},
},
want: map[string][]string{},
},
{
name: "no header name",
args: args{
anns: map[string]string{
"konghq.com/headers/": "foo",
},
},
want: map[string][]string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := ExtractHeaders(tt.args.anns)
if len(tt.want) == 0 {
assert.False(t, ok)
} else {
assert.True(t, ok)
}
for key, val := range tt.want {
actual, ok := got[key]
assert.True(t, ok)
assert.Equal(t, val, actual)
}
})
}
}

func TestExtractPathHandling(t *testing.T) {
type args struct {
anns map[string]string
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
want: "",
},
{
name: "non-empty",
args: args{
anns: map[string]string{
"konghq.com/path-handling": "v1",
},
},
want: "v1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := ExtractPathHandling(tt.args.anns)
if tt.want == "" {
assert.False(t, ok)
} else {
assert.True(t, ok)
}
if got != tt.want {
t.Errorf("ExtractPathHandling() = %v, want %v", got, tt.want)
}
})
}
}
27 changes: 26 additions & 1 deletion internal/dataplane/kongstate/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ type Route struct {
}

var (
validMethods = regexp.MustCompile(`\A[A-Z]+$`)
validMethods = regexp.MustCompile(`\A[A-Z]+$`)
rainest marked this conversation as resolved.
Show resolved Hide resolved
validPathHandling = regexp.MustCompile(`v\d`)

// 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.
Expand Down Expand Up @@ -228,6 +229,8 @@ func (r *Route) overrideByAnnotation(log logrus.FieldLogger) {
r.overrideRequestBuffering(log, r.Ingress.Annotations)
r.overrideResponseBuffering(log, r.Ingress.Annotations)
r.overrideHosts(log, r.Ingress.Annotations)
r.overrideHeaders(r.Ingress.Annotations)
r.overridePathHandling(log, r.Ingress.Annotations)
}

// override sets Route fields by KongIngress first, then by annotation.
Expand Down Expand Up @@ -405,3 +408,25 @@ func (r *Route) overrideHosts(log logrus.FieldLogger, anns map[string]string) {

r.Hosts = hosts
}

func (r *Route) overrideHeaders(anns map[string]string) {
headers, exists := annotations.ExtractHeaders(anns)
if !exists {
return
}
r.Headers = headers
}

func (r *Route) overridePathHandling(log logrus.FieldLogger, anns map[string]string) {
val, ok := annotations.ExtractPathHandling(anns)
if !ok {
return
}

if !validPathHandling.MatchString(val) {
log.WithField("kongroute", r.Name).Errorf("invalid path_handling value: %s", val)
return
}

r.PathHandling = kong.String(val)
}
Loading