From 506c2cf20530d317155f645d798d16e0f300b08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Burzy=C5=84ski?= Date: Tue, 5 Dec 2023 14:34:34 +0100 Subject: [PATCH] docs: add KongServiceFacade docs and examples --- CHANGELOG.md | 1 + FEATURE_GATES.md | 86 +++++++ ...troller.konghq.com_kongservicefacades.yaml | 10 +- docs/incubator-api-reference.md | 62 +++++ examples/kong-service-facade.yaml | 215 ++++++++++++++++++ .../v1alpha1/kongservicefacade_types.go | 3 + scripts/apidocs-gen/generate.sh | 20 +- test/e2e/all_in_one_test.go | 3 +- test/e2e/helpers_test.go | 54 +---- test/e2e/upgrade_test.go | 2 +- test/integration/isolated/consts.go | 2 +- .../isolated/examples_grpc_test.go | 2 +- .../examples_kong_service_facade_test.go | 150 ++++++++++++ .../isolated/examples_udproute_test.go | 2 +- test/internal/helpers/deployment.go | 65 ++++++ test/internal/testlabels/labels.go | 11 +- 16 files changed, 617 insertions(+), 71 deletions(-) create mode 100644 docs/incubator-api-reference.md create mode 100644 examples/kong-service-facade.yaml create mode 100644 test/integration/isolated/examples_kong_service_facade_test.go create mode 100644 test/internal/helpers/deployment.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a0ca4d1d..1a35fcb478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ Adding a new version? You'll need three changes: [#5234](https://github.com/Kong/kubernetes-ingress-controller/pull/5234) [#5290](https://github.com/Kong/kubernetes-ingress-controller/pull/5290) [#5282](https://github.com/Kong/kubernetes-ingress-controller/pull/5282) + [#5298](https://github.com/Kong/kubernetes-ingress-controller/pull/5298) [#5302](https://github.com/Kong/kubernetes-ingress-controller/pull/5302) - Added support for GRPC over HTTP (without TLS) in Gateway API. [#5128](https://github.com/Kong/kubernetes-ingress-controller/pull/5128) diff --git a/FEATURE_GATES.md b/FEATURE_GATES.md index bbb77c97af..d4a7821e88 100644 --- a/FEATURE_GATES.md +++ b/FEATURE_GATES.md @@ -168,3 +168,89 @@ services: if two HTTPRoute rules use both serviceA and serviceB in their backendRefs, KIC will generate a single Kong service with endpoints from both serviceA and serviceB, named for the first rule and match indices with that combination of Services. + +## Using KongServiceFacade + +In KIC 3.1.0 we introduced a new feature called `KongServiceFacade`. Currently, we only +support `KongServiceFacade` to be used as a backend for `netv1.Ingress`es. +If you find this might be useful to you in other contexts (e.g. Gateway API's `HTTPRoute`s), +please let us know in the [#5216](https://github.com/Kong/kubernetes-ingress-controller/issues/5216) +issue tracking this effort. + +### Installation + +`KongServiceFacade` feature is currently in `Alpha` maturity and is disabled by default. +To start using it, you must not only enable the feature gate (`KongServiceFacade=true`), +but also install the `KongServiceFacade` CRD which is distributed in a separate +package we named `incubator`. You can install it by running: + +```shell +kubectl apply -k github.com/Kong/kubernetes-ingress-controller/main/config/crd/incubator?ref=main +``` + +### Usage + +Once the CRD is installed and the feature gate is set to `true`, you can start using it by creating +`KongServiceFacade` objects and use them as your `netv1.Ingress` backends. For example, to create +a `KongServiceFacade` that points to a service named `my-service` in the `default` namespace and uses +its port number `80`, you can run: + +```shell +kubectl apply -f - < + +## Packages +- [incubator.ingress-controller.konghq.com/v1alpha1](#incubatoringress-controllerkonghqcomv1alpha1) + + +## incubator.ingress-controller.konghq.com/v1alpha1 + +Package v1alpha1 contains API Schema definitions for the incubator.ingress-controller.konghq.com v1alpha1 API group. + +- [KongServiceFacade](#kongservicefacade) + +### KongServiceFacade + + + +KongServiceFacade allows creating separate Kong Services for a single Kubernetes Service. It can be used as Kubernetes Ingress' backend (via its path's `backend.resource` field). It's designed to enable creating two "virtual" Services in Kong that will point to the same Kubernetes Service, but will have different configuration (e.g. different set of plugins, different load balancing algorithm, etc.).

KongServiceFacade requires `kubernetes.io/ingress.class` annotation with a value matching the ingressClass of the Kong Ingress Controller (`kong` by default) to be reconciled. + + + +| Field | Description | +| --- | --- | +| `apiVersion` _string_ | `incubator.ingress-controller.konghq.com/v1alpha1` +| `kind` _string_ | `KongServiceFacade` +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | +| `spec` _[KongServiceFacadeSpec](#kongservicefacadespec)_ | | + + + + +### KongServiceFacadeBackend + + + +KongServiceFacadeBackend is a reference to a Kubernetes Service that is used as a backend for a Kong Service Facade. + + + +| Field | Description | +| --- | --- | +| `name` _string_ | Name is the name of the referenced Kubernetes Service. | +| `port` _integer_ | Port is the port of the referenced Kubernetes Service. | + + +_Appears in:_ +- [KongServiceFacadeSpec](#kongservicefacadespec) + +### KongServiceFacadeSpec + + + +KongServiceFacadeSpec defines the desired state of KongServiceFacade. + + + +| Field | Description | +| --- | --- | +| `backendRef` _[KongServiceFacadeBackend](#kongservicefacadebackend)_ | Backend is a reference to a Kubernetes Service that is used as a backend for this Kong Service Facade. | + + +_Appears in:_ +- [KongServiceFacade](#kongservicefacade) diff --git a/examples/kong-service-facade.yaml b/examples/kong-service-facade.yaml new file mode 100644 index 0000000000..aee572a831 --- /dev/null +++ b/examples/kong-service-facade.yaml @@ -0,0 +1,215 @@ +# This example demonstrates how to use the KongServiceFacade resource to +# configure Kong to route traffic to a Kubernetes Service, and secure it +# with a different plugin for each KongServiceFacade. +# +# To verify the example: +# +# 1. Install the KongServiceFacade CRD: +# $ kubectl apply -k "github.com/Kong/kubernetes-ingress-controller/config/crd/incubator?ref=main" +# +# 2. Install the Kong Ingress Controller with KongServiceFacade feature gate enabled: +# $ helm upgrade --install kong kong/ingress -n kong --create-namespace \ +# --set controller.ingressController.env.feature_gates=KongServiceFacade=true \ +# --set controller.ingressController.image.repository=kong/nightly-ingress-controller \ +# --set controller.ingressController.image.tag=2023-12-07 \ +# --set controller.ingressController.image.effectiveSemver=3.1.0 +# +# 3. Apply the example manifest: +# +# $ kubectl apply -f https://raw.githubusercontent.com/Kong/kubernetes-ingress-controller/main/examples/kong-service-facade.yaml +# +# 4. Wait for KongServiceFacades to be configured in Kong: +# +# $ kubectl wait --for=condition=Programmed=True kongservicefacade/svc-facade-alpha kongservicefacade/svc-facade-beta +# +# 5. Get Proxy's Service external IP: +# +# $ export PROXY_IP=$(kubectl get service -n kong kong-gateway-proxy -o=jsonpath='{.status.loadBalancer.ingress[0].ip}') +# $ echo $PROXY_IP # To ensure that the variable is set. +# 198.19.249.2 +# +# 6. Verify that paths /alpha and /beta are secured with key-auth and basic-auth respectively (we're using httpie here): +# +# $ http -h ${PROXY_IP}/alpha key:alice-password # /alpha allows valid key. +# HTTP/1.1 200 OK +# $ http -h ${PROXY_IP}/alpha key:wrong-key # /alpha doesn't allow invalid key. +# HTTP/1.1 401 Unauthorized +# $ http -h ${PROXY_IP}/alpha -a bob:bob-password # /alpha doesn't allow valid basic-auth credentials. +# HTTP/1.1 401 Unauthorized +# $ http -h ${PROXY_IP}/beta -a bob:bob-password # /beta allows valid basic-auth credentials. +# HTTP/1.1 200 OK +# $ http -h ${PROXY_IP}/beta -a bob:wrong # /beta doesn't allow invalid basic-auth credentials. +# HTTP/1.1 401 Unauthorized +# $ http -h ${PROXY_IP}/beta key:alice-password # /beta doesn't allow valid key. +# HTTP/1.1 401 Unauthorized +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: httpbin-deployment + namespace: default + labels: + app: httpbin +spec: + replicas: 1 + selector: + matchLabels: + app: httpbin + template: + metadata: + labels: + app: httpbin + spec: + containers: + - name: httpbin + image: kong/httpbin:0.1.0 + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: httpbin + name: httpbin-deployment + namespace: default +spec: + ports: + - port: 80 + protocol: TCP + targetPort: 80 + selector: + app: httpbin + type: ClusterIP +--- +# KongServiceFacade pointing to the httpbin-deployment, +# secured with key-auth plugin. +apiVersion: incubator.ingress-controller.konghq.com/v1alpha1 +kind: KongServiceFacade +metadata: + name: svc-facade-alpha + namespace: default + annotations: + kubernetes.io/ingress.class: kong + konghq.com/plugins: key-auth +spec: + backendRef: + name: httpbin-deployment + port: 80 +--- +# KongServiceFacade pointing to the same httpbin-deployment, +# secured with basic-auth plugin. +apiVersion: incubator.ingress-controller.konghq.com/v1alpha1 +kind: KongServiceFacade +metadata: + name: svc-facade-beta + namespace: default + annotations: + kubernetes.io/ingress.class: kong + konghq.com/plugins: basic-auth +spec: + backendRef: + name: httpbin-deployment + port: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: httpbin-ingress + namespace: default + annotations: + konghq.com/strip-path: "true" +spec: + ingressClassName: kong + rules: + - http: + paths: + - path: /alpha + pathType: Prefix + backend: + # The /alpha path uses the KongServiceFacade secured with key-auth plugin. + resource: + apiGroup: incubator.ingress-controller.konghq.com + kind: KongServiceFacade + name: svc-facade-alpha + - path: /beta + pathType: Prefix + backend: + # The /beta path uses the KongServiceFacade secured with basic-auth plugin. + resource: + apiGroup: incubator.ingress-controller.konghq.com + kind: KongServiceFacade + name: svc-facade-beta +--- +# The following resources are used to configure the key-auth and basic-auth plugins +# used by the KongServiceFacade resources. +# +# For key-auth it sets up a KongConsumer `alice` that can be authenticated by +# setting the `key` header to `alice-password`. +# +# For basic-auth it sets up a KongConsumer `bob` that can be authenticated by +# setting the `Authorization` header to `Basic `. +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: key-auth + namespace: default +plugin: key-auth +config: + key_names: ["key"] +--- +apiVersion: v1 +kind: Secret +metadata: + name: alice-credentials + namespace: default + annotations: + kubernetes.io/ingress.class: kong + labels: + konghq.com/credential: key-auth +type: Opaque +stringData: + key: "alice-password" +--- +apiVersion: configuration.konghq.com/v1 +kind: KongConsumer +metadata: + name: alice + namespace: default + annotations: + kubernetes.io/ingress.class: kong +username: alice +credentials: + - alice-credentials +--- +apiVersion: configuration.konghq.com/v1 +kind: KongPlugin +metadata: + name: basic-auth + namespace: default +plugin: basic-auth +--- +apiVersion: v1 +kind: Secret +metadata: + name: bob-credentials + namespace: default + annotations: + kubernetes.io/ingress.class: kong + labels: + konghq.com/credential: basic-auth +type: Opaque +stringData: + username: "bob" + password: "bob-password" +--- +apiVersion: configuration.konghq.com/v1 +kind: KongConsumer +metadata: + name: bob + namespace: default + annotations: + kubernetes.io/ingress.class: kong +username: bob +credentials: + - bob-credentials diff --git a/pkg/apis/incubator/v1alpha1/kongservicefacade_types.go b/pkg/apis/incubator/v1alpha1/kongservicefacade_types.go index b81dc698af..568a384c1a 100644 --- a/pkg/apis/incubator/v1alpha1/kongservicefacade_types.go +++ b/pkg/apis/incubator/v1alpha1/kongservicefacade_types.go @@ -19,6 +19,9 @@ func init() { // to the same Kubernetes Service, but will have different configuration (e.g. different // set of plugins, different load balancing algorithm, etc.). // +// KongServiceFacade requires `kubernetes.io/ingress.class` annotation with a value +// matching the ingressClass of the Kong Ingress Controller (`kong` by default) to be reconciled. +// // +genclient // +kubebuilder:object:root=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/scripts/apidocs-gen/generate.sh b/scripts/apidocs-gen/generate.sh index a6c62a9f51..4e1ca737d8 100755 --- a/scripts/apidocs-gen/generate.sh +++ b/scripts/apidocs-gen/generate.sh @@ -7,10 +7,16 @@ set -o pipefail SCRIPT_ROOT="$(dirname "${BASH_SOURCE[0]}")/../.." CRD_REF_DOCS_BIN="$1" -${CRD_REF_DOCS_BIN} \ - --source-path="${SCRIPT_ROOT}/pkg/apis/configuration/" \ - --config="${SCRIPT_ROOT}/scripts/apidocs-gen/config.yaml" \ - --templates-dir="${SCRIPT_ROOT}/scripts/apidocs-gen/template" \ - --renderer=markdown \ - --output-path="${SCRIPT_ROOT}/docs/api-reference.md" \ - --max-depth=10 +generate() { + echo "INFO: generating API docs for ${1} package, output: ${2}" + ${CRD_REF_DOCS_BIN} \ + --source-path="${SCRIPT_ROOT}${1}" \ + --config="${SCRIPT_ROOT}/scripts/apidocs-gen/config.yaml" \ + --templates-dir="${SCRIPT_ROOT}/scripts/apidocs-gen/template" \ + --renderer=markdown \ + --output-path="${SCRIPT_ROOT}${2}" \ + --max-depth=10 +} + +generate "/pkg/apis/configuration" "/docs/api-reference.md" +generate "/pkg/apis/incubator" "/docs/incubator-api-reference.md" diff --git a/test/e2e/all_in_one_test.go b/test/e2e/all_in_one_test.go index 91260834f8..18230441da 100644 --- a/test/e2e/all_in_one_test.go +++ b/test/e2e/all_in_one_test.go @@ -24,6 +24,7 @@ import ( k8stypes "k8s.io/apimachinery/pkg/types" "github.com/kong/kubernetes-ingress-controller/v3/internal/metrics" + "github.com/kong/kubernetes-ingress-controller/v3/test/internal/helpers" ) // ----------------------------------------------------------------------------- @@ -321,7 +322,7 @@ func TestDeployAllInOneDBLESS(t *testing.T) { t.Log("restart the controller") deployments.RestartController(ctx, t, env) - waitForDeploymentRollout(ctx, t, env, namespace, controllerDeploymentName) + helpers.WaitForDeploymentRollout(ctx, t, env.Cluster(), namespace, controllerDeploymentName) t.Log("scale proxy to 3 replicas and verify that the new replica gets the old good configuration") scaleDeployment(ctx, t, env, deployments.ProxyNN, 3) diff --git a/test/e2e/helpers_test.go b/test/e2e/helpers_test.go index 361a825159..a1635fd5b2 100644 --- a/test/e2e/helpers_test.go +++ b/test/e2e/helpers_test.go @@ -283,8 +283,8 @@ func (d ManifestDeploy) Run(ctx context.Context, t *testing.T, env environments. t.Log("waiting for controller to be ready") deployments := getManifestDeployments(d.Path) - waitForDeploymentRollout(ctx, t, env, deployments.ControllerNN.Namespace, deployments.ControllerNN.Name) - waitForDeploymentRollout(ctx, t, env, deployments.ProxyNN.Namespace, deployments.ProxyNN.Name) + helpers.WaitForDeploymentRollout(ctx, t, env.Cluster(), deployments.ControllerNN.Namespace, deployments.ControllerNN.Name) + helpers.WaitForDeploymentRollout(ctx, t, env.Cluster(), deployments.ProxyNN.Namespace, deployments.ProxyNN.Name) return deployments } @@ -312,52 +312,6 @@ func (d ManifestDeploy) Delete(ctx context.Context, t *testing.T, env environmen require.NoError(t, err, string(out)) } -func waitForDeploymentRollout(ctx context.Context, t *testing.T, env environments.Environment, namespace, name string) { - require.Eventuallyf(t, func() bool { - deployment, err := env.Cluster().Client().AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false - } - - if err := allUpdatedReplicasRolledOutAndReady(deployment); err != nil { - t.Logf("%s/%s deployment not ready: %s", namespace, name, err) - return false - } - - return true - }, kongComponentWait, time.Second, "deployment %s/%s didn't roll out in time", namespace, name) -} - -// allUpdatedReplicasRolledOutAndReady ensures that all updated replicas are rolled out and ready. It is to make sure -// that the deployment rollout is finished and all the new replicas are ready to serve traffic before we proceed with -// the test. It returns an error with a reason if the deployment is not ready yet. -func allUpdatedReplicasRolledOutAndReady(d *appsv1.Deployment) error { - if newReplicasRolledOut := d.Spec.Replicas != nil && d.Status.UpdatedReplicas < *d.Spec.Replicas; newReplicasRolledOut { - return fmt.Errorf( - "%d out of %d new replicas have been updated", - d.Status.UpdatedReplicas, - *d.Spec.Replicas, - ) - } - - if oldReplicasPendingTermination := d.Status.Replicas > d.Status.UpdatedReplicas; oldReplicasPendingTermination { - return fmt.Errorf( - "%d old replicas pending termination", - d.Status.Replicas-d.Status.UpdatedReplicas, - ) - } - - if rolloutFinished := d.Status.AvailableReplicas == d.Status.UpdatedReplicas; !rolloutFinished { - return fmt.Errorf( - "%d of %d updated replicas are available", - d.Status.AvailableReplicas, - d.Status.UpdatedReplicas, - ) - } - - return nil -} - // Deployments represent the deployments that are deployed by the all-in-one manifests. type Deployments struct { ProxyNN k8stypes.NamespacedName @@ -919,6 +873,6 @@ func (d Deployments) Restart(ctx context.Context, t *testing.T, env environments }) require.NoError(t, err, "failed to delete proxy pods") - waitForDeploymentRollout(ctx, t, env, d.ControllerNN.Namespace, d.ControllerNN.Name) - waitForDeploymentRollout(ctx, t, env, d.ProxyNN.Namespace, d.ProxyNN.Name) + helpers.WaitForDeploymentRollout(ctx, t, env.Cluster(), d.ControllerNN.Namespace, d.ControllerNN.Name) + helpers.WaitForDeploymentRollout(ctx, t, env.Cluster(), d.ProxyNN.Namespace, d.ProxyNN.Name) } diff --git a/test/e2e/upgrade_test.go b/test/e2e/upgrade_test.go index f0db974dce..157c1da2d4 100644 --- a/test/e2e/upgrade_test.go +++ b/test/e2e/upgrade_test.go @@ -129,7 +129,7 @@ func testManifestsUpgrade( variableName: "CONTROLLER_FEATURE_GATES", value: featureGates, })) - waitForDeploymentRollout(ctx, t, env, deployments.ControllerNN.Namespace, deployments.ControllerNN.Name) + helpers.WaitForDeploymentRollout(ctx, t, env.Cluster(), deployments.ControllerNN.Namespace, deployments.ControllerNN.Name) } t.Log("creating new ingress with new path /echo-new") diff --git a/test/integration/isolated/consts.go b/test/integration/isolated/consts.go index 410f3a60f9..cec091bd85 100644 --- a/test/integration/isolated/consts.go +++ b/test/integration/isolated/consts.go @@ -4,6 +4,6 @@ package isolated import "fmt" -func manifestPath(manifestName string) string { +func examplesManifestPath(manifestName string) string { return fmt.Sprintf("../../../examples/%s", manifestName) } diff --git a/test/integration/isolated/examples_grpc_test.go b/test/integration/isolated/examples_grpc_test.go index 0e4a650d47..feef6e5b5c 100644 --- a/test/integration/isolated/examples_grpc_test.go +++ b/test/integration/isolated/examples_grpc_test.go @@ -23,7 +23,7 @@ func TestGRPCRouteExample(t *testing.T) { testGRPC := func(ctx context.Context, t *testing.T, manifestName string, gatewayPort int, hostname string, enableTLS bool) { cluster := GetClusterFromCtx(ctx) proxyURL := GetProxyURLFromCtx(ctx) - manifestPath := manifestPath(manifestName) + manifestPath := examplesManifestPath(manifestName) t.Logf("applying yaml manifest %s", manifestPath) b, err := os.ReadFile(manifestPath) assert.NoError(t, err) diff --git a/test/integration/isolated/examples_kong_service_facade_test.go b/test/integration/isolated/examples_kong_service_facade_test.go new file mode 100644 index 0000000000..d35da597ba --- /dev/null +++ b/test/integration/isolated/examples_kong_service_facade_test.go @@ -0,0 +1,150 @@ +//go:build integration_tests + +package isolated + +import ( + "context" + "fmt" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/kong/kubernetes-testing-framework/pkg/clusters" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" + + "github.com/kong/kubernetes-ingress-controller/v3/test/internal/helpers" + "github.com/kong/kubernetes-ingress-controller/v3/test/internal/testlabels" +) + +func TestKongServiceFacadeExample(t *testing.T) { + f := features. + New("example"). + WithLabel(testlabels.Example, testlabels.ExampleTrue). + WithLabel(testlabels.NetworkingFamily, testlabels.NetworkingFamilyIngress). + WithLabel(testlabels.Kind, testlabels.KindKongServiceFacade). + WithSetup("deploy kong addon into cluster", featureSetup( + withControllerManagerOpts(helpers.ControllerManagerOptAdditionalWatchNamespace("default")), + )). + WithSetup("deploy example manifest", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + manifestPath := examplesManifestPath("kong-service-facade.yaml") + + b, err := os.ReadFile(manifestPath) + require.NoError(t, err) + manifest := string(b) + + ingressClass := GetIngressClassFromCtx(ctx) + + t.Logf("replacing kong ingress class in yaml manifest to %s", ingressClass) + manifest = strings.ReplaceAll( + manifest, + "kubernetes.io/ingress.class: kong", + fmt.Sprintf("kubernetes.io/ingress.class: %s", ingressClass), + ) + manifest = strings.ReplaceAll( + manifest, + "ingressClassName: kong", + fmt.Sprintf("ingressClassName: %s", ingressClass), + ) + + t.Logf("applying yaml manifest %s", manifestPath) + cluster := GetClusterFromCtx(ctx) + require.NoError(t, clusters.ApplyManifestByYAML(ctx, cluster, manifest)) + + t.Cleanup(func() { + t.Logf("deleting yaml manifest %s", manifestPath) + assert.NoError(t, clusters.DeleteManifestByYAML(ctx, cluster, manifest)) + }) + + t.Log("waiting for httpbin deployment to be ready") + helpers.WaitForDeploymentRollout(ctx, t, cluster, "default", "httpbin-deployment") + return ctx + }). + Assess("basic-auth and key-auth plugins are applied to KongServiceFacades as expected", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + var ( + proxyURL = GetProxyURLFromCtx(ctx) + + keyAuthSecuredEndpoint = fmt.Sprintf("%s%s", proxyURL, "/alpha") + keyAuthValidKey = "alice-password" + + basicAuthSecuredEndpoint = fmt.Sprintf("%s%s", proxyURL, "/beta") + basicAuthValidUsername = "bob" + basicAuthValidPassword = "bob-password" + ) + + httpClient := helpers.DefaultHTTPClient() + respondsWithExpectedStatusCode := func(t *testing.T, req *http.Request, expectedStatusCode int) { + require.Eventually(t, func() bool { + res, err := httpClient.Do(req) + if err != nil { + t.Logf("%s request returned an error: %s", req.URL, err) + return false + } + defer res.Body.Close() + + if res.StatusCode != expectedStatusCode { + t.Logf("%s request returned unexpected status code %d instead of %d", req.URL, res.StatusCode, expectedStatusCode) + return false + } + + return true + }, time.Minute, 250*time.Millisecond) + } + + newRequest := func(endpoint string, modFn func(*http.Request)) *http.Request { + req := lo.Must(http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)) + modFn(req) + return req + } + + t.Run("key-auth endpoint responses", func(t *testing.T) { + t.Log("ensuring key-auth endpoint allows a valid key") + validKeyAuthReq := newRequest(keyAuthSecuredEndpoint, func(r *http.Request) { + r.Header.Set("key", keyAuthValidKey) + }) + respondsWithExpectedStatusCode(t, validKeyAuthReq, http.StatusOK) + + t.Log("ensuring key-auth endpoint doesn't allow an invalid key") + invalidKeyAuthReq := newRequest(keyAuthSecuredEndpoint, func(r *http.Request) { + r.Header.Set("key", "invalid-pass") + }) + respondsWithExpectedStatusCode(t, invalidKeyAuthReq, http.StatusUnauthorized) + + t.Log("ensuring key-auth endpoint doesn't allow valid basic-auth credentials") + invalidKeyAuthUsingBasicAuthReq := newRequest(keyAuthSecuredEndpoint, func(r *http.Request) { + r.SetBasicAuth(basicAuthValidUsername, basicAuthValidPassword) + }) + respondsWithExpectedStatusCode(t, invalidKeyAuthUsingBasicAuthReq, http.StatusUnauthorized) + }) + + t.Run("basic-auth endpoint responses", func(t *testing.T) { + t.Log("ensuring basic-auth endpoint allows valid credentials") + validBasicAuthReq := newRequest(basicAuthSecuredEndpoint, func(r *http.Request) { + r.SetBasicAuth(basicAuthValidUsername, basicAuthValidPassword) + }) + respondsWithExpectedStatusCode(t, validBasicAuthReq, http.StatusOK) + + t.Log("ensuring basic-auth endpoint doesn't allow invalid credentials") + invalidBasicAuthReq := newRequest(basicAuthSecuredEndpoint, func(r *http.Request) { + r.SetBasicAuth(basicAuthValidUsername, "invalid-pass") + }) + respondsWithExpectedStatusCode(t, invalidBasicAuthReq, http.StatusUnauthorized) + + t.Log("ensuring basic-auth endpoint doesn't allow a valid key-auth key") + invalidBasicAuthUsingKeyAuthReq := newRequest(basicAuthSecuredEndpoint, func(r *http.Request) { + r.Header.Set("key", keyAuthValidKey) + }) + respondsWithExpectedStatusCode(t, invalidBasicAuthUsingKeyAuthReq, http.StatusUnauthorized) + }) + + return ctx + }). + Teardown(featureTeardown()) + + tenv.Test(t, f.Feature()) +} diff --git a/test/integration/isolated/examples_udproute_test.go b/test/integration/isolated/examples_udproute_test.go index d835c39495..72497b48e0 100644 --- a/test/integration/isolated/examples_udproute_test.go +++ b/test/integration/isolated/examples_udproute_test.go @@ -21,7 +21,7 @@ import ( func TestExampleUDPRoute(t *testing.T) { t.Parallel() - udpRouteExampleManifests := manifestPath("gateway-udproute.yaml") + udpRouteExampleManifests := examplesManifestPath("gateway-udproute.yaml") f := features. New("example"). diff --git a/test/internal/helpers/deployment.go b/test/internal/helpers/deployment.go new file mode 100644 index 0000000000..699bb609a6 --- /dev/null +++ b/test/internal/helpers/deployment.go @@ -0,0 +1,65 @@ +package helpers + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/kong/kubernetes-testing-framework/pkg/clusters" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + kongComponentRolloutTimeout = 7 * time.Minute +) + +// WaitForDeploymentRollout waits for the deployment to roll out in the cluster. It fails the test if the deployment +// doesn't roll out in time. +func WaitForDeploymentRollout(ctx context.Context, t *testing.T, cluster clusters.Cluster, namespace, name string) { + require.Eventuallyf(t, func() bool { + deployment, err := cluster.Client().AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false + } + + if err := allUpdatedReplicasRolledOutAndReady(deployment); err != nil { + t.Logf("%s/%s deployment not ready: %s", namespace, name, err) + return false + } + + return true + }, kongComponentRolloutTimeout, time.Second, "deployment %s/%s didn't roll out in time", namespace, name) +} + +// allUpdatedReplicasRolledOutAndReady ensures that all updated replicas are rolled out and ready. It is to make sure +// that the deployment rollout is finished and all the new replicas are ready to serve traffic before we proceed with +// the test. It returns an error with a reason if the deployment is not ready yet. +func allUpdatedReplicasRolledOutAndReady(d *appsv1.Deployment) error { + if newReplicasRolledOut := d.Spec.Replicas != nil && d.Status.UpdatedReplicas < *d.Spec.Replicas; newReplicasRolledOut { + return fmt.Errorf( + "%d out of %d new replicas have been updated", + d.Status.UpdatedReplicas, + *d.Spec.Replicas, + ) + } + + if oldReplicasPendingTermination := d.Status.Replicas > d.Status.UpdatedReplicas; oldReplicasPendingTermination { + return fmt.Errorf( + "%d old replicas pending termination", + d.Status.Replicas-d.Status.UpdatedReplicas, + ) + } + + if rolloutFinished := d.Status.AvailableReplicas == d.Status.UpdatedReplicas; !rolloutFinished { + return fmt.Errorf( + "%d of %d updated replicas are available", + d.Status.AvailableReplicas, + d.Status.UpdatedReplicas, + ) + } + + return nil +} diff --git a/test/internal/testlabels/labels.go b/test/internal/testlabels/labels.go index 1965fe980a..c5cf387a42 100644 --- a/test/internal/testlabels/labels.go +++ b/test/internal/testlabels/labels.go @@ -2,11 +2,12 @@ package testlabels const ( // Kind is the label key used to store the primary kind that's being tested. - Kind = "kind" - KindUDPRoute = "UDPRoute" - KindTCPRoute = "TCPRoute" - KindGRPCRoute = "GRPCRoute" - KindIngress = "Ingress" + Kind = "kind" + KindUDPRoute = "UDPRoute" + KindTCPRoute = "TCPRoute" + KindGRPCRoute = "GRPCRoute" + KindIngress = "Ingress" + KindKongServiceFacade = "KongServiceFacade" ) const (