Skip to content

Commit 551528e

Browse files
rainestpmalek
andauthored
feat(annotations) support additional fields (#3121)
Add annotations for fields previously supported only in KongIngress. Add connect, write, and read timeout annotations to Services. Add retries annotation to Services. Add headers annotation to Routes. The annotation is in the form "konghq.com/headers/example: value1,value2", where the portion of the annoation name after "konghq.com/headers" ("example") is the header name and the value is a CSV of header values. Add path handling annotation to Routes. Co-authored-by: Patryk Małek <[email protected]>
1 parent 6988320 commit 551528e

File tree

7 files changed

+786
-1
lines changed

7 files changed

+786
-1
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ Adding a new version? You'll need three changes:
102102
irrelevant secrets (e.g: service account tokens) are not stored. This change
103103
is made to reduce memory usage of the cache.
104104
[#3047](https://github.com/Kong/kubernetes-ingress-controller/pull/3047)
105+
- Services support annotations for connect, read, and write timeouts.
106+
[#3121](https://github.com/Kong/kubernetes-ingress-controller/pull/3121)
107+
- Services support annotations for retries.
108+
[#3121](https://github.com/Kong/kubernetes-ingress-controller/pull/3121)
109+
- Routes support annotations for headers.
110+
[#3121](https://github.com/Kong/kubernetes-ingress-controller/pull/3121)
111+
- Routes support annotations for path handling.
112+
[#3121](https://github.com/Kong/kubernetes-ingress-controller/pull/3121)
105113

106114
### Fixed
107115

internal/annotations/annotations.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ const (
5656
ResponseBuffering = "/response-buffering"
5757
HostAliasesKey = "/host-aliases"
5858
RegexPrefixKey = "/regex-prefix"
59+
ConnectTimeoutKey = "/connect-timeout"
60+
WriteTimeoutKey = "/write-timeout"
61+
ReadTimeoutKey = "/read-timeout"
62+
RetriesKey = "/retries"
63+
HeadersKey = "/headers"
64+
PathHandlingKey = "/path-handling"
5965

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

262+
// ExtractConnectTimeout extracts the connection timeout annotation value.
263+
func ExtractConnectTimeout(anns map[string]string) (string, bool) {
264+
val, exists := anns[AnnotationPrefix+ConnectTimeoutKey]
265+
if !exists {
266+
return "", false
267+
}
268+
return val, true
269+
}
270+
271+
// ExtractWriteTimeout extracts the write timeout annotation value.
272+
func ExtractWriteTimeout(anns map[string]string) (string, bool) {
273+
val, exists := anns[AnnotationPrefix+WriteTimeoutKey]
274+
if !exists {
275+
return "", false
276+
}
277+
return val, true
278+
}
279+
280+
// ExtractReadTimeout extracts the read timeout annotation value.
281+
func ExtractReadTimeout(anns map[string]string) (string, bool) {
282+
val, exists := anns[AnnotationPrefix+ReadTimeoutKey]
283+
if !exists {
284+
return "", false
285+
}
286+
return val, true
287+
}
288+
289+
// ExtractRetries extracts the retries annotation value.
290+
func ExtractRetries(anns map[string]string) (string, bool) {
291+
val, exists := anns[AnnotationPrefix+RetriesKey]
292+
if !exists {
293+
return "", false
294+
}
295+
return val, true
296+
}
297+
298+
// ExtractHeaders extracts the parsed headers annotations values. It returns a map of header names to slices of values.
299+
func ExtractHeaders(anns map[string]string) (map[string][]string, bool) {
300+
headers := make(map[string][]string)
301+
prefix := AnnotationPrefix + HeadersKey + "/"
302+
for key, val := range anns {
303+
if strings.HasPrefix(key, prefix) {
304+
header := strings.TrimPrefix(key, prefix)
305+
if len(header) == 0 || len(val) == 0 {
306+
continue
307+
}
308+
headers[header] = strings.Split(val, ",")
309+
}
310+
}
311+
if len(headers) == 0 {
312+
return headers, false
313+
}
314+
return headers, true
315+
}
316+
317+
// ExtractPathHandling extracts the path handling annotation value.
318+
func ExtractPathHandling(anns map[string]string) (string, bool) {
319+
val, exists := anns[AnnotationPrefix+PathHandlingKey]
320+
if !exists {
321+
return "", false
322+
}
323+
return val, true
324+
}
325+
256326
// ExtractUnmanagedGatewayClassMode extracts the value of the unmanaged gateway
257327
// mode annotation.
258328
func ExtractUnmanagedGatewayClassMode(anns map[string]string) string {

internal/annotations/annotations_test.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"testing"
2222

2323
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
2425
corev1 "k8s.io/api/core/v1"
2526
extensions "k8s.io/api/extensions/v1beta1"
2627
netv1 "k8s.io/api/networking/v1"
@@ -684,3 +685,245 @@ func TestExtractHostAliases(t *testing.T) {
684685
})
685686
}
686687
}
688+
689+
func TestExtractConnectTimeout(t *testing.T) {
690+
type args struct {
691+
anns map[string]string
692+
}
693+
tests := []struct {
694+
name string
695+
args args
696+
want string
697+
}{
698+
{
699+
name: "empty",
700+
want: "",
701+
},
702+
{
703+
name: "non-empty",
704+
args: args{
705+
anns: map[string]string{
706+
"konghq.com/connect-timeout": "3000",
707+
},
708+
},
709+
want: "3000",
710+
},
711+
}
712+
for _, tt := range tests {
713+
t.Run(tt.name, func(t *testing.T) {
714+
got, ok := ExtractConnectTimeout(tt.args.anns)
715+
if tt.want == "" {
716+
assert.False(t, ok)
717+
} else {
718+
assert.True(t, ok)
719+
require.Equal(t, tt.want, got)
720+
}
721+
})
722+
}
723+
}
724+
725+
func TestExtractWriteTimeout(t *testing.T) {
726+
type args struct {
727+
anns map[string]string
728+
}
729+
tests := []struct {
730+
name string
731+
args args
732+
want string
733+
}{
734+
{
735+
name: "empty",
736+
want: "",
737+
},
738+
{
739+
name: "non-empty",
740+
args: args{
741+
anns: map[string]string{
742+
"konghq.com/write-timeout": "3000",
743+
},
744+
},
745+
want: "3000",
746+
},
747+
}
748+
for _, tt := range tests {
749+
t.Run(tt.name, func(t *testing.T) {
750+
got, ok := ExtractWriteTimeout(tt.args.anns)
751+
if tt.want == "" {
752+
assert.False(t, ok)
753+
} else {
754+
assert.True(t, ok)
755+
require.Equal(t, tt.want, got)
756+
}
757+
})
758+
}
759+
}
760+
761+
func TestExtractReadTimeout(t *testing.T) {
762+
type args struct {
763+
anns map[string]string
764+
}
765+
tests := []struct {
766+
name string
767+
args args
768+
want string
769+
}{
770+
{
771+
name: "empty",
772+
want: "",
773+
},
774+
{
775+
name: "non-empty",
776+
args: args{
777+
anns: map[string]string{
778+
"konghq.com/read-timeout": "3000",
779+
},
780+
},
781+
want: "3000",
782+
},
783+
}
784+
for _, tt := range tests {
785+
t.Run(tt.name, func(t *testing.T) {
786+
got, ok := ExtractReadTimeout(tt.args.anns)
787+
if tt.want == "" {
788+
assert.False(t, ok)
789+
} else {
790+
assert.True(t, ok)
791+
require.Equal(t, tt.want, got)
792+
}
793+
})
794+
}
795+
}
796+
797+
func TestExtractRetries(t *testing.T) {
798+
type args struct {
799+
anns map[string]string
800+
}
801+
tests := []struct {
802+
name string
803+
args args
804+
want string
805+
}{
806+
{
807+
name: "empty",
808+
want: "",
809+
},
810+
{
811+
name: "non-empty",
812+
args: args{
813+
anns: map[string]string{
814+
"konghq.com/retries": "3000",
815+
},
816+
},
817+
want: "3000",
818+
},
819+
}
820+
for _, tt := range tests {
821+
t.Run(tt.name, func(t *testing.T) {
822+
got, ok := ExtractRetries(tt.args.anns)
823+
if tt.want == "" {
824+
assert.False(t, ok)
825+
} else {
826+
assert.True(t, ok)
827+
}
828+
if got != tt.want {
829+
t.Errorf("ExtractRetries() = %v, want %v", got, tt.want)
830+
}
831+
})
832+
}
833+
}
834+
835+
func TestExtractHeaders(t *testing.T) {
836+
type args struct {
837+
anns map[string]string
838+
}
839+
tests := []struct {
840+
name string
841+
args args
842+
want map[string][]string
843+
}{
844+
{
845+
name: "empty",
846+
want: map[string][]string{},
847+
},
848+
{
849+
name: "non-empty",
850+
args: args{
851+
anns: map[string]string{
852+
"konghq.com/headers/foo": "foo",
853+
},
854+
},
855+
want: map[string][]string{"foo": {"foo"}},
856+
},
857+
{
858+
name: "no separator",
859+
args: args{
860+
anns: map[string]string{
861+
"konghq.com/headersfoo": "foo",
862+
},
863+
},
864+
want: map[string][]string{},
865+
},
866+
{
867+
name: "no header name",
868+
args: args{
869+
anns: map[string]string{
870+
"konghq.com/headers/": "foo",
871+
},
872+
},
873+
want: map[string][]string{},
874+
},
875+
}
876+
for _, tt := range tests {
877+
t.Run(tt.name, func(t *testing.T) {
878+
got, ok := ExtractHeaders(tt.args.anns)
879+
if len(tt.want) == 0 {
880+
assert.False(t, ok)
881+
} else {
882+
assert.True(t, ok)
883+
}
884+
for key, val := range tt.want {
885+
actual, ok := got[key]
886+
assert.True(t, ok)
887+
assert.Equal(t, val, actual)
888+
}
889+
})
890+
}
891+
}
892+
893+
func TestExtractPathHandling(t *testing.T) {
894+
type args struct {
895+
anns map[string]string
896+
}
897+
tests := []struct {
898+
name string
899+
args args
900+
want string
901+
}{
902+
{
903+
name: "empty",
904+
want: "",
905+
},
906+
{
907+
name: "non-empty",
908+
args: args{
909+
anns: map[string]string{
910+
"konghq.com/path-handling": "v1",
911+
},
912+
},
913+
want: "v1",
914+
},
915+
}
916+
for _, tt := range tests {
917+
t.Run(tt.name, func(t *testing.T) {
918+
got, ok := ExtractPathHandling(tt.args.anns)
919+
if tt.want == "" {
920+
assert.False(t, ok)
921+
} else {
922+
assert.True(t, ok)
923+
}
924+
if got != tt.want {
925+
t.Errorf("ExtractPathHandling() = %v, want %v", got, tt.want)
926+
}
927+
})
928+
}
929+
}

internal/dataplane/kongstate/route.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ type Route struct {
2424
}
2525

2626
var (
27-
validMethods = regexp.MustCompile(`\A[A-Z]+$`)
27+
validMethods = regexp.MustCompile(`\A[A-Z]+$`)
28+
validPathHandling = regexp.MustCompile(`v\d`)
2829

2930
// hostnames are complicated. shamelessly cribbed from https://stackoverflow.com/a/18494710
3031
// TODO if the Kong core adds support for wildcard SNI route match criteria, this should change.
@@ -228,6 +229,8 @@ func (r *Route) overrideByAnnotation(log logrus.FieldLogger) {
228229
r.overrideRequestBuffering(log, r.Ingress.Annotations)
229230
r.overrideResponseBuffering(log, r.Ingress.Annotations)
230231
r.overrideHosts(log, r.Ingress.Annotations)
232+
r.overrideHeaders(r.Ingress.Annotations)
233+
r.overridePathHandling(log, r.Ingress.Annotations)
231234
}
232235

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

406409
r.Hosts = hosts
407410
}
411+
412+
func (r *Route) overrideHeaders(anns map[string]string) {
413+
headers, exists := annotations.ExtractHeaders(anns)
414+
if !exists {
415+
return
416+
}
417+
r.Headers = headers
418+
}
419+
420+
func (r *Route) overridePathHandling(log logrus.FieldLogger, anns map[string]string) {
421+
val, ok := annotations.ExtractPathHandling(anns)
422+
if !ok {
423+
return
424+
}
425+
426+
if !validPathHandling.MatchString(val) {
427+
log.WithField("kongroute", r.Name).Errorf("invalid path_handling value: %s", val)
428+
return
429+
}
430+
431+
r.PathHandling = kong.String(val)
432+
}

0 commit comments

Comments
 (0)