diff --git a/Makefile b/Makefile index cd9b055..85dd8c8 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ OCTOPS_BIN := bin/octops-controller IMAGE_REPO=octops/gameserver-ingress-controller DOCKER_IMAGE_TAG ?= octops/gameserver-ingress-controller:${VERSION} -RELEASE_TAG=0.2.0 +RELEASE_TAG=0.2.1 default: clean build diff --git a/README.md b/README.md index a8832ed..1493117 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,8 @@ spec: template: metadata: annotations: - octops-kubernetes.io/ingress.class: "contour" # required for Contour to handle ingress - octops-projectcontour.io/websocket-routes: "/" # required for Contour to enable websocket + octops-kubernetes.io/ingress.class: "contour" #required for Contour to handle ingress + octops-projectcontour.io/websocket-routes: "/" #required for Contour to enable websocket octops.io/gameserver-ingress-mode: "domain" octops.io/gameserver-ingress-domain: "example.com" ``` @@ -84,8 +84,8 @@ spec: template: metadata: annotations: - octops-kubernetes.io/ingress.class: "contour" # required for Contour to handle ingress - octops-projectcontour.io/websocket-routes: "/" # required for Contour to enable websocket + octops-kubernetes.io/ingress.class: "contour" #required for Contour to handle ingress + octops-projectcontour.io/websocket-routes: "/{{ .Name }}" #required for Contour to enable websocket for exact path. This is a template that the controller will replace by the name of the game server octops.io/gameserver-ingress-mode: "path" octops.io/gameserver-ingress-fqdn: servers.example.com ``` @@ -175,6 +175,16 @@ Will be added to the ingress in the following format: `projectcontour.io/websocket-routes`: `/` +It is also possible to use a template to fill values at the Ingress creation time. This feature is specially useful if the routing mode is `path`. +Envoy will only enable websocket for routes that match exactly the path set on the Ingress rules. + +The example below demonstrates how custom annotations using template would be generated for a game server named `octops-tl6hf-fnmgd`. + +Custom Annotation: `octops-projectcontour.io/websocket-routes`: `/{{ .Name }}` +Final Annotation: `octops-projectcontour.io/websocket-routes`: `/octops-tl6hf-fnmgd` + +The same applies for any other custom annotation. In the future more fields will be added but now `.Name` is the only one supported. + **Any annotation can be used. It is not restricted to the [Contour controller annotations](https://projectcontour.io/docs/main/config/annotations/)**. `octops-my-custom-annotations`: `my-custom-value` will be passed to the Ingress resource as: diff --git a/deploy/install.yaml b/deploy/install.yaml index fc00ac4..207af6a 100644 --- a/deploy/install.yaml +++ b/deploy/install.yaml @@ -69,7 +69,7 @@ spec: spec: serviceAccountName: octops containers: - - image: octops/gameserver-ingress-controller:0.2.0 # Latest release + - image: octops/gameserver-ingress-controller:0.2.1 # Latest release name: controller ports: - containerPort: 30235 diff --git a/examples/fleet-dev.yaml b/examples/fleet-dev.yaml index 7568db7..bc9784d 100644 --- a/examples/fleet-dev.yaml +++ b/examples/fleet-dev.yaml @@ -20,7 +20,9 @@ metadata: cluster: gke-1.22 region: us-east-1 spec: - replicas: 3 +# strategy: +# type: Recreate + replicas: 1 template: metadata: labels: @@ -29,8 +31,10 @@ spec: annotations: # octops.io/gameserver-ingress-mode: "domain" # octops.io/gameserver-ingress-domain: "example.com" + octops.service-traefik.ingress.kubernetes.io/service.serversscheme: "h2c" octops-kubernetes.io/ingress.class: "contour" # required for Contour to handle ingress - octops-projectcontour.io/websocket-routes: "/" # required for Contour to enable websocket +# octops-projectcontour.io/websocket-routes: "/" # required for Contour to enable websocket + octops-projectcontour.io/websocket-routes: "/{{ .Name }}" # use template to define values octops.io/gameserver-ingress-mode: "path" octops.io/gameserver-ingress-fqdn: "servers.example.com" spec: @@ -46,10 +50,12 @@ spec: - name: gameserver imagePullPolicy: Always image: ksdn117/web-socket-test +# image: gcr.io/agones-images/udp-server:0.21 +# image: gcr.io/agones-images/udp-server:0.22 resources: requests: - memory: "64Mi" - cpu: "20m" + memory: "1Mi" + cpu: "0.02m" limits: memory: "64Mi" - cpu: "20m" \ No newline at end of file + cpu: "2m" \ No newline at end of file diff --git a/examples/fleet-path.yaml b/examples/fleet-path.yaml index 13f0254..f109877 100644 --- a/examples/fleet-path.yaml +++ b/examples/fleet-path.yaml @@ -27,10 +27,10 @@ spec: cluster: gke-1.17 region: us-east-1 annotations: - octops-kubernetes.io/ingress.class: "contour" # required for Contour to handle ingress - octops-projectcontour.io/websocket-routes: "/" # required for Contour to enable websocket + octops-kubernetes.io/ingress.class: "contour" #required for Contour to handle ingress octops.io/gameserver-ingress-mode: "path" octops.io/gameserver-ingress-fqdn: "servers.example.com" + octops-projectcontour.io/websocket-routes: "/{{ .Name }}" #required for Contour to enable websocket, use template to define values #octops.io/tls-secret-name: "custom-secret" #octops.io/terminate-tls: "true" #octops.io/issuer-tls-name: "selfsigned-issuer" diff --git a/pkg/reconcilers/ingress_reconciler.go b/pkg/reconcilers/ingress_reconciler.go index 746b28c..ca7885e 100644 --- a/pkg/reconcilers/ingress_reconciler.go +++ b/pkg/reconcilers/ingress_reconciler.go @@ -51,6 +51,7 @@ func (r *IngressReconciler) reconcileNotFound(ctx context.Context, gs *agonesv1. opts := []IngressOption{ WithCustomAnnotations(), + WithCustomAnnotationsTemplate(), WithIngressRule(mode), WithTLS(mode), WithTLSCertIssuer(issuer), diff --git a/pkg/reconcilers/options.go b/pkg/reconcilers/options.go index 0013942..5be2935 100644 --- a/pkg/reconcilers/options.go +++ b/pkg/reconcilers/options.go @@ -8,10 +8,49 @@ import ( networkingv1 "k8s.io/api/networking/v1" "strconv" "strings" + "text/template" ) type IngressOption func(gs *agonesv1.GameServer, ingress *networkingv1.Ingress) error +func WithCustomAnnotationsTemplate() IngressOption { + return func(gs *agonesv1.GameServer, ingress *networkingv1.Ingress) error { + data := struct { + Name string + }{ + Name: gs.Name, + } + + annotations := ingress.Annotations + for k, v := range gs.Annotations { + if strings.HasPrefix(k, gameserver.OctopsAnnotationCustomPrefix) { + custom := strings.TrimPrefix(k, gameserver.OctopsAnnotationCustomPrefix) + if len(custom) == 0 { + return errors.New("custom annotation does not contain a suffix") + } + + if !strings.Contains(v, "{{") || !strings.Contains(v, "}}") { + continue + } + + t, err := template.New("gs").Parse(v) + if err != nil { + return errors.Errorf("%s:%s does not contain a valid template", custom, v) + } + + b := new(strings.Builder) + err = t.Execute(b, data) + if parsed := b.String(); len(parsed) > 0 { + annotations[custom] = parsed + } + } + } + + ingress.SetAnnotations(annotations) + return nil + } +} + func WithCustomAnnotations() IngressOption { return func(gs *agonesv1.GameServer, ingress *networkingv1.Ingress) error { annotations := ingress.Annotations diff --git a/pkg/reconcilers/options_test.go b/pkg/reconcilers/options_test.go index 9250f06..b6b2a2b 100644 --- a/pkg/reconcilers/options_test.go +++ b/pkg/reconcilers/options_test.go @@ -8,6 +8,179 @@ import ( "testing" ) +func Test_WithCustomAnnotationsTemplate(t *testing.T) { + testCase := []struct { + name string + gameserverName string + annotations map[string]string + wantErr bool + expected map[string]string + }{ + { + name: "with not custom annotations", + gameserverName: "game-1", + annotations: map[string]string{ + "annotation/not_custom": "somevalue", + }, + wantErr: false, + expected: map[string]string{}, + }, + { + name: "with custom annotation without template", + gameserverName: "game-2", + annotations: map[string]string{ + "octops-annotation/custom": "somevalue", + }, + wantErr: false, + expected: map[string]string{}, + }, + { + name: "with custom annotation with template only", + gameserverName: "game-3", + annotations: map[string]string{ + "octops-annotation/custom": "{{ .Name }}", + }, + wantErr: false, + expected: map[string]string{ + "annotation/custom": "game-3", + }, + }, + { + name: "with custom annotation with complex template", + gameserverName: "game-4", + annotations: map[string]string{ + "octops-annotation/custom": "/{{ .Name }}", + }, + wantErr: false, + expected: map[string]string{ + "annotation/custom": "/game-4", + }, + }, + { + name: "with custom annotation with invalid template", + gameserverName: "game-5", + annotations: map[string]string{ + "octops-annotation/custom": "}}{{", + }, + expected: nil, + wantErr: true, + }, + { + name: "with custom annotation with invalid template", + gameserverName: "game-6", + annotations: map[string]string{ + "octops-annotation/custom": "{{}}", + }, + wantErr: true, + }, + { + name: "with custom annotation with invalid mixed template", + gameserverName: "game-7", + annotations: map[string]string{ + "octops-annotation/custom": "{{ /.Name}}", + }, + wantErr: true, + }, + { + name: "with custom annotation with invalid field", + gameserverName: "game-8", + annotations: map[string]string{ + "octops-annotation/custom": "{{ .SomeField }}", + }, + expected: map[string]string{}, + wantErr: false, + }, + { + name: "with not custom annotation with template", + gameserverName: "game-9", + annotations: map[string]string{ + "annotation/not-custom": "{{ .SomeField }}", + }, + wantErr: false, + expected: map[string]string{}, + }, + { + name: "with custom envoy annotation with template", + gameserverName: "game-10", + annotations: map[string]string{ + "octops-projectcontour.io/websocket-routes": "/{{ .Name }}", + }, + wantErr: false, + expected: map[string]string{ + "projectcontour.io/websocket-routes": "/game-10", + }, + }, + { + name: "with multiples annotations", + gameserverName: "game-10", + annotations: map[string]string{ + "annotation/not-custom": "somevalue", + "octops-projectcontour.io/websocket-routes": "/{{ .Name }}", + }, + wantErr: false, + expected: map[string]string{ + "projectcontour.io/websocket-routes": "/game-10", + }, + }, + { + name: "with multiples annotations inverted", + gameserverName: "game-11", + annotations: map[string]string{ + "octops-projectcontour.io/websocket-routes": "/{{ .Name }}", + "annotation/not-custom": "somevalue", + }, + wantErr: false, + expected: map[string]string{ + "projectcontour.io/websocket-routes": "/game-11", + }, + }, + { + name: "with multiples annotations with template", + gameserverName: "game-12", + annotations: map[string]string{ + "octops-projectcontour.io/websocket-routes": "/{{ .Name }}", + "octops-annotation/custom": "custom-{{ .Name }}", + }, + wantErr: false, + expected: map[string]string{ + "projectcontour.io/websocket-routes": "/game-12", + "annotation/custom": "custom-game-12", + }, + }, + { + name: "with mixed annotations with template", + gameserverName: "game-13", + annotations: map[string]string{ + "annotation/not-custom": "some-value", + "annotation/not-custom-template": "some-{{ .Name }}", + "octops-projectcontour.io/websocket-routes": "/{{ .Name }}", + "octops-annotation/custom": "custom-{{ .Name }}", + }, + wantErr: false, + expected: map[string]string{ + "projectcontour.io/websocket-routes": "/game-13", + "annotation/custom": "custom-game-13", + }, + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + gs := newGameServer(tc.gameserverName, "default", tc.annotations) + require.Equal(t, tc.gameserverName, gs.Name) + + ingress, err := newIngress(gs, WithCustomAnnotationsTemplate()) + if tc.wantErr { + require.Error(t, err) + require.Nil(t, ingress) + } else { + require.NoError(t, err) + require.Equal(t, tc.expected, ingress.Annotations) + } + }) + } +} + func Test_WithCustomAnnotations(t *testing.T) { newCustomAnnotation := func(custom string) string { return fmt.Sprintf("%s%s", gameserver.OctopsAnnotationCustomPrefix, custom)