diff --git a/WALKTHROUGH.md b/WALKTHROUGH.md index 5279f15..dfef5d2 100644 --- a/WALKTHROUGH.md +++ b/WALKTHROUGH.md @@ -117,11 +117,13 @@ spec: entryPoints: - web routes: - - match: Host(`walkthrough.docker.localhost`) && Path(`/no-auth`) + - match: Host(`walkthrough.docker.localhost`) && PathPrefix(`/no-auth`) kind: Rule services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather ``` ```shell @@ -135,17 +137,15 @@ ingressroute.traefik.io/walkthrough-weather-api created This API can be accessed using curl: ```shell -curl http://walkthrough.docker.localhost/no-auth +curl http://walkthrough.docker.localhost/no-auth/weather ``` ```json -{ - "public": [ - { "id": 1, "city": "GopherCity", "weather": "Moderate rain" }, - { "id": 2, "city": "City of Gophers", "weather": "Sunny" }, - { "id": 3, "city": "GopherRocks", "weather": "Cloudy" } - ] -} +[ + {"city":"GopherTown","id":"0","weather":"Cloudy"}, + {"city":"City of Gophers","id":"1","weather":"Sunny"}, + {"city":"GopherRocks","id":"2","weather":"Cloudy"} +] ``` With Traefik Proxy, we can secure the access to this API using the Basic Authentication. To create an encoded _user_:_password_ pair, we can use `htpasswd` with `openssl` to encode it. @@ -163,7 +163,7 @@ Zm9vOiRhcHIxJDJHR0RyLjJPJDdUVXJlOEt6anQ1WFFOUGRoby5CQjEKCg== ```diff :hack/diff.sh -r -a "-Nau src/manifests/walkthrough/weather-app-no-auth.yaml src/manifests/walkthrough/weather-app-basic-auth.yaml" --- src/manifests/walkthrough/weather-app-no-auth.yaml +++ src/manifests/walkthrough/weather-app-basic-auth.yaml -@@ -1,15 +1,37 @@ +@@ -1,17 +1,38 @@ --- +apiVersion: v1 +kind: Secret @@ -195,18 +195,21 @@ Zm9vOiRhcHIxJDJHR0RyLjJPJDdUVXJlOEt6anQ1WFFOUGRoby5CQjEKCg== entryPoints: - web routes: -- - match: Host(`walkthrough.docker.localhost`) && Path(`/no-auth`) +- - match: Host(`walkthrough.docker.localhost`) && PathPrefix(`/no-auth`) - kind: Rule - services: - - name: weather-app - port: 3000 -+ - match: Host(`walkthrough.docker.localhost`) && Path(`/basic-auth`) +- middlewares: +- - name: stripprefix-weather ++ - match: Host(`walkthrough.docker.localhost`) && PathPrefix(`/basic-auth`) + kind: Rule + services: + - name: weather-app + port: 3000 + middlewares: -+ - name: basic-auth ++ - name: stripprefix-weather ++ - name: basic-auth ``` Let's apply it: @@ -225,9 +228,9 @@ And now, we can confirm it's secured using BASIC Authentication : ```shell # This call is not authorized => 401 -curl -I http://walkthrough.docker.localhost/basic-auth +curl -i http://walkthrough.docker.localhost/basic-auth/weather # This call is allowed => 200 -curl -I -u foo:bar http://walkthrough.docker.localhost/basic-auth +curl -i -u foo:bar http://walkthrough.docker.localhost/basic-auth/weather ``` [Basic Authentication](https://datatracker.ietf.org/doc/html/rfc7617) worked and was widely used in the early days of the web. However, it also has a security risk: @@ -276,9 +279,9 @@ And also confirm _Basic Auth_ is still here: ```shell # This call is not authorized => 401 -curl -I http://walkthrough.docker.localhost/basic-auth +curl -i http://walkthrough.docker.localhost/basic-auth/weather # This call is allowed => 200 -curl -I -u foo:bar http://walkthrough.docker.localhost/basic-auth +curl -i -u foo:bar http://walkthrough.docker.localhost/basic-auth/weather ``` Let's secure the weather API with an API Key. @@ -299,7 +302,7 @@ We can now put this password in the API Key middleware: ```diff :hack/diff.sh -r -a "-Nau src/manifests/walkthrough/weather-app-no-auth.yaml src/manifests/walkthrough/weather-app-apikey.yaml" --- src/manifests/walkthrough/weather-app-no-auth.yaml +++ src/manifests/walkthrough/weather-app-apikey.yaml -@@ -1,15 +1,41 @@ +@@ -1,17 +1,42 @@ --- +apiVersion: v1 +kind: Secret @@ -335,18 +338,21 @@ We can now put this password in the API Key middleware: entryPoints: - web routes: -- - match: Host(`walkthrough.docker.localhost`) && Path(`/no-auth`) +- - match: Host(`walkthrough.docker.localhost`) && PathPrefix(`/no-auth`) - kind: Rule - services: - - name: weather-app - port: 3000 -+ - match: Host(`walkthrough.docker.localhost`) && Path(`/api-key`) +- middlewares: +- - name: stripprefix-weather ++ - match: Host(`walkthrough.docker.localhost`) && PathPrefix(`/api-key`) + kind: Rule + services: + - name: weather-app + port: 3000 + middlewares: -+ - name: walkthrough-apikey-auth ++ - name: stripprefix-weather ++ - name: walkthrough-apikey-auth ``` Let's apply it: @@ -365,11 +371,11 @@ And test it: ```shell # This call is not authorized => 401 -curl -I http://walkthrough.docker.localhost/api-key +curl -i http://walkthrough.docker.localhost/api-key/weather # Let's set the token export API_KEY=$(echo -n "Let's use API Key with Traefik Hub" | base64) # This call with the token is allowed => 200 -curl -I -H "Authorization: Bearer $API_KEY" http://walkthrough.docker.localhost/api-key +curl -i -H "Authorization: Bearer $API_KEY" http://walkthrough.docker.localhost/api-key/weather ``` The API is now secured. @@ -399,9 +405,9 @@ And also confirm that the API is still secured using an API Key: ```shell # This call is not authorized => 401 -curl -I http://walkthrough.docker.localhost/api-key +curl -i http://walkthrough.docker.localhost/api-key/weather # This call with the token is allowed => 200 -curl -I -H "Authorization: Bearer $API_KEY" http://walkthrough.docker.localhost/api-key +curl -i -H "Authorization: Bearer $API_KEY" http://walkthrough.docker.localhost/api-key/weather ``` Now, let's try to manage it with Traefik Hub using `API` and `APIAccess` resources: @@ -418,7 +424,7 @@ spec: path: /openapi.yaml override: servers: - - url: http://api.getting-started.apimanagement.docker.localhost + - url: http://api.walkthrough.docker.localhost --- apiVersion: hub.traefik.io/v1alpha1 @@ -447,7 +453,7 @@ spec: entryPoints: - web routes: - - match: Host(`api.walkthrough.docker.localhost`) && PathRegexp(`^/weather(/([0-9]+|openapi.yaml))?$`) + - match: Host(`api.walkthrough.docker.localhost`) && PathPrefix(`/weather`) kind: Rule services: - name: weather-app @@ -563,13 +569,11 @@ curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.walkthrough.docker.local ``` ```json -{ - "public": [ - { "id": 1, "city": "GopherCity", "weather": "Moderate rain" }, - { "id": 2, "city": "City of Gophers", "weather": "Sunny" }, - { "id": 3, "city": "GopherRocks", "weather": "Cloudy" } - ] -} +[ + {"city":"GopherTown","id":"0","weather":"Cloudy"}, + {"city":"City of Gophers","id":"1","weather":"Sunny"}, + {"city":"GopherRocks","id":"2","weather":"Cloudy"} +] ``` :information_source: If it fails with 401, wait one minute and try again. The token needs to be sync before it can be accepted by Traefik Hub. @@ -580,10 +584,6 @@ We can see the API available in the `apps` namespace in the portal. We advise ev However, it's still possible not setting an OAS, but it severely hurts getting started with API consumption. -```shell -kubectl apply -f src/manifests/walkthrough/api.yaml -``` - This time, we won't specify any OAS in the API _CRD_: ```yaml :src/manifests/walkthrough/forecast.yaml -s 1 -e 7 diff --git a/api-gateway/1-getting-started/README.md b/api-gateway/1-getting-started/README.md index d247f79..1376be0 100644 --- a/api-gateway/1-getting-started/README.md +++ b/api-gateway/1-getting-started/README.md @@ -110,7 +110,7 @@ kubectl apply --server-side --force-conflicts -k https://github.com/traefik/trae # Update the Helm repository helm repo update # Upgrade the Helm chart -helm upgrade traefik-hub -n traefik-hub --wait \ +helm upgrade traefik -n traefik --wait \ --set hub.token=traefik-hub-license \ --set ingressClass.enabled=false \ --set ingressRoute.dashboard.enabled=true \ @@ -144,6 +144,7 @@ It should create the public app ```shell namespace/apps created configmap/weather-data created +middleware.traefik.io/stripprefix-weather created deployment.apps/weather-app created service/weather-app created configmap/weather-app-openapispec created @@ -162,11 +163,13 @@ spec: entryPoints: - web routes: - - match: Host(`getting-started.apigateway.docker.localhost`) + - match: Host(`getting-started.apigateway.docker.localhost`) && PathPrefix(`/weather`) kind: Rule services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather ``` ```shell @@ -180,17 +183,15 @@ ingressroute.traefik.io/getting-started-apigateway created This API can be accessed using curl: ```shell -curl http://getting-started.apigateway.docker.localhost/ +curl http://getting-started.apigateway.docker.localhost/weather ``` ```json -{ - "public": [ - { "id": 1, "city": "GopherCity", "weather": "Moderate rain" }, - { "id": 2, "city": "City of Gophers", "weather": "Sunny" }, - { "id": 3, "city": "GopherRocks", "weather": "Cloudy" } - ] -} +[ + {"city":"GopherCity","id":"0","weather":"Moderate rain"}, + {"city":"City of Gophers","id":"1","weather":"Sunny"}, + {"city":"GopherRocks","id":"2","weather":"Cloudy"} +] ``` ### Step 3: Secure authentication on this API with Traefik Hub @@ -213,7 +214,7 @@ Put this hash in the API Key `Middleware`: ```diff :../../hack/diff.sh -r -a "manifests/weather-app-ingressroute.yaml manifests/weather-app-apikey.yaml" --- manifests/weather-app-ingressroute.yaml +++ manifests/weather-app-apikey.yaml -@@ -1,15 +1,41 @@ +@@ -1,17 +1,42 @@ --- +apiVersion: v1 +kind: Secret @@ -249,13 +250,15 @@ Put this hash in the API Key `Middleware`: entryPoints: - web routes: -- - match: Host(`getting-started.apigateway.docker.localhost`) -+ - match: Host(`getting-started.apigateway.docker.localhost`) && Path(`/api-key`) +- - match: Host(`getting-started.apigateway.docker.localhost`) && PathPrefix(`/weather`) ++ - match: Host(`getting-started.apigateway.docker.localhost`) && PathPrefix(`/api-key`) kind: Rule services: - name: weather-app port: 3000 -+ middlewares: + middlewares: +- - name: stripprefix-weather ++ - name: stripprefix-weather + - name: getting-started-apigateway-apikey-auth ``` @@ -275,11 +278,11 @@ And test it: ```shell # This call is not authorized => 401 -curl -I http://getting-started.apigateway.docker.localhost/api-key +curl -i http://getting-started.apigateway.docker.localhost/api-key/weather # Let's set the API key export API_KEY=$(echo -n "Let's use API Key with Traefik Hub" | base64) # This call with the token is allowed => 200 -curl -I -H "Authorization: Bearer $API_KEY" http://getting-started.apigateway.docker.localhost/api-key +curl -i -H "Authorization: Bearer $API_KEY" http://getting-started.apigateway.docker.localhost/api-key/weather ``` The API is now secured. diff --git a/api-gateway/1-getting-started/manifests/weather-app-apikey.yaml b/api-gateway/1-getting-started/manifests/weather-app-apikey.yaml index e90664b..bec63f3 100644 --- a/api-gateway/1-getting-started/manifests/weather-app-apikey.yaml +++ b/api-gateway/1-getting-started/manifests/weather-app-apikey.yaml @@ -32,10 +32,11 @@ spec: entryPoints: - web routes: - - match: Host(`getting-started.apigateway.docker.localhost`) && Path(`/api-key`) + - match: Host(`getting-started.apigateway.docker.localhost`) && PathPrefix(`/api-key`) kind: Rule services: - name: weather-app port: 3000 middlewares: + - name: stripprefix-weather - name: getting-started-apigateway-apikey-auth diff --git a/api-gateway/1-getting-started/manifests/weather-app-ingressroute.yaml b/api-gateway/1-getting-started/manifests/weather-app-ingressroute.yaml index 1cc449b..149b498 100644 --- a/api-gateway/1-getting-started/manifests/weather-app-ingressroute.yaml +++ b/api-gateway/1-getting-started/manifests/weather-app-ingressroute.yaml @@ -8,8 +8,10 @@ spec: entryPoints: - web routes: - - match: Host(`getting-started.apigateway.docker.localhost`) + - match: Host(`getting-started.apigateway.docker.localhost`) && PathPrefix(`/weather`) kind: Rule services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather diff --git a/api-gateway/2-secure-applications/m2m.md b/api-gateway/2-secure-applications/m2m.md index c1f5583..622db93 100644 --- a/api-gateway/2-secure-applications/m2m.md +++ b/api-gateway/2-secure-applications/m2m.md @@ -261,7 +261,7 @@ Let's try it: ```shell kubectl apply -f api-gateway/2-secure-applications/manifests/whoami-app-oauth2-client-creds-nologin.yaml -sleep 2 +sleep 3 curl http://secure-applications.apigateway.docker.localhost/oauth2-client-credentials-nologin ``` diff --git a/api-management/1-getting-started/README.md b/api-management/1-getting-started/README.md index 7f32d50..216c62f 100644 --- a/api-management/1-getting-started/README.md +++ b/api-management/1-getting-started/README.md @@ -104,7 +104,7 @@ kubectl apply --server-side --force-conflicts -k https://github.com/traefik/trae # Update the Helm repository helm repo add --force-update traefik https://traefik.github.io/charts # Upgrade the Helm chart -helm upgrade traefik-hub -n traefik --wait \ +helm upgrade traefik -n traefik --wait \ --set hub.token=traefik-hub-license \ --set hub.apimanagement.enabled=true \ --set ingressClass.enabled=false \ @@ -137,6 +137,7 @@ It creates the weather app: ```shell namespace/apps unchanged configmap/weather-data unchanged +middleware.traefik.io/stripprefix-weather unchanged deployment.apps/weather-app unchanged service/weather-app unchanged configmap/weather-app-openapispec unchanged @@ -160,6 +161,8 @@ spec: services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather ``` ```shell @@ -173,17 +176,15 @@ ingressroute.traefik.io/getting-started-apimanagement created At this moment, this API is exposed. It's possible to reach it using `curl` command: ```shell -curl http://getting-started.apimanagement.docker.localhost +curl http://getting-started.apimanagement.docker.localhost/weather ``` ```json -{ - "public": [ - { "id": 1, "city": "GopherCity", "weather": "Moderate rain" }, - { "id": 2, "city": "City of Gophers", "weather": "Sunny" }, - { "id": 3, "city": "GopherRocks", "weather": "Cloudy" } - ] -} +[ + {"city":"City of Gophers","id":"1","weather":"Sunny"}, + {"city":"GopherRocks","id":"2","weather":"Cloudy"}, + {"city":"GopherCity","id":"0","weather":"Moderate rain"} +] ``` ## Step 3: Manage the API using Traefik Hub API Management @@ -231,7 +232,7 @@ spec: entryPoints: - web routes: - - match: Host(`api.getting-started.apimanagement.docker.localhost`) && PathRegexp(`^/weather(/([0-9]+|openapi.yaml))?$`) + - match: Host(`api.getting-started.apimanagement.docker.localhost`) && PathPrefix(`/weather`) kind: Rule services: - name: weather-app @@ -345,13 +346,11 @@ curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.getting-started.apimanag ``` ```json -{ - "public": [ - { "id": 1, "city": "GopherCity", "weather": "Moderate rain" }, - { "id": 2, "city": "City of Gophers", "weather": "Sunny" }, - { "id": 3, "city": "GopherRocks", "weather": "Cloudy" } - ] -} +[ + {"city":"City of Gophers","id":"1","weather":"Sunny"}, + {"city":"GopherRocks","id":"2","weather":"Cloudy"}, + {"city":"GopherCity","id":"0","weather":"Moderate rain"} +] ``` :information_source: If it fails with 401, wait a minute and try again. The token needs to be sync before it can be accepted by Traefik Hub. diff --git a/api-management/1-getting-started/manifests/api.yaml b/api-management/1-getting-started/manifests/api.yaml index b215a4e..6070738 100644 --- a/api-management/1-getting-started/manifests/api.yaml +++ b/api-management/1-getting-started/manifests/api.yaml @@ -34,7 +34,7 @@ spec: entryPoints: - web routes: - - match: Host(`api.getting-started.apimanagement.docker.localhost`) && PathRegexp(`^/weather(/([0-9]+|openapi.yaml))?$`) + - match: Host(`api.getting-started.apimanagement.docker.localhost`) && PathPrefix(`/weather`) kind: Rule services: - name: weather-app diff --git a/api-management/1-getting-started/manifests/weather-app-ingressroute.yaml b/api-management/1-getting-started/manifests/weather-app-ingressroute.yaml index 55359ec..fca211e 100644 --- a/api-management/1-getting-started/manifests/weather-app-ingressroute.yaml +++ b/api-management/1-getting-started/manifests/weather-app-ingressroute.yaml @@ -13,3 +13,5 @@ spec: services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather diff --git a/api-management/2-access-control/README.md b/api-management/2-access-control/README.md index da94175..2705e21 100644 --- a/api-management/2-access-control/README.md +++ b/api-management/2-access-control/README.md @@ -64,12 +64,13 @@ spec: entryPoints: - web routes: - - match: Host(`api.access-control.apimanagement.docker.localhost`) && Path(`/simple/admin`) + - match: Host(`api.access-control.apimanagement.docker.localhost`) && PathPrefix(`/simple/admin`) kind: Rule services: - name: admin-app port: 3000 - + middlewares: + - name: stripprefix-admin ``` ```shell @@ -127,6 +128,8 @@ spec: services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather ``` ```shell @@ -194,7 +197,7 @@ One needs to define operationSets to configure operationFilters. Here, we'll dif ```diff :../../hack/diff.sh -r -a "manifests/simple-weather-api.yaml manifests/complex-weather-api.yaml" --- manifests/simple-weather-api.yaml +++ manifests/complex-weather-api.yaml -@@ -2,41 +2,71 @@ +@@ -2,40 +2,69 @@ apiVersion: hub.traefik.io/v1alpha1 kind: API metadata: @@ -209,11 +212,11 @@ One needs to define operationSets to configure operationFilters. Here, we'll dif + operationSets: + - name: get-forecast + matchers: -+ - pathPrefix: "/complex/weather" ++ - pathPrefix: "/weather" + methods: [ "GET" ] + - name: patch-forecast + matchers: -+ - pathPrefix: "/complex/weather" ++ - pathPrefix: "/weather/0" + methods: [ "PATCH" ] --- @@ -270,8 +273,6 @@ One needs to define operationSets to configure operationFilters. Here, we'll dif kind: Rule services: - name: weather-app - port: 3000 -+ ``` ### Deploy and test it @@ -291,7 +292,7 @@ curl -i -H "Authorization: Bearer $ADMIN_TOKEN" "http://api.access-control.apima # This call is now allowed curl -i -H "Authorization: Bearer $ADMIN_TOKEN" "http://api.access-control.apimanagement.docker.localhost/complex/weather" # And even PATCH is allowed -curl -i -XPATCH -H "Authorization: Bearer $ADMIN_TOKEN" "http://api.access-control.apimanagement.docker.localhost/complex/weather" +curl -i -XPATCH -H "Authorization: Bearer $ADMIN_TOKEN" "http://api.access-control.apimanagement.docker.localhost/complex/weather/0" -d '[{"op": "replace", "path": "/city", "value": "GopherTown"}]' ``` And test it with the external user's token: @@ -300,7 +301,7 @@ And test it with the external user's token: # This one is allowed curl -i -H "Authorization: Bearer $EXTERNAL_TOKEN" "http://api.access-control.apimanagement.docker.localhost/complex/weather" # And PATCH should be not allowed -curl -i -XPATCH -H "Authorization: Bearer $EXTERNAL_TOKEN" "http://api.access-control.apimanagement.docker.localhost/complex/weather" +curl -i -XPATCH -H "Authorization: Bearer $EXTERNAL_TOKEN" "http://api.access-control.apimanagement.docker.localhost/complex/weather/0" -d '[{"op": "replace", "path": "/weather", "value": "Cloudy"}]' ``` It can be explained quite easily if **PATCH** is still allowed. There is still an `APIAccess` created with the simple tutorial: @@ -359,5 +360,5 @@ The first one allows all kinds of HTTP requests. If we delete it, the _external_ ```shell kubectl delete apiaccess -n apps access-control-apimanagement-simple-weather # This time, PATCH is not allowed -curl -i -XPATCH -H "Authorization: Bearer $EXTERNAL_TOKEN" "http://api.access-control.apimanagement.docker.localhost/complex/weather" +curl -i -XPATCH -H "Authorization: Bearer $EXTERNAL_TOKEN" "http://api.access-control.apimanagement.docker.localhost/complex/weather/0" -d '[{"op": "replace", "path": "/weather", "value": "Cloudy"}]' ``` diff --git a/api-management/2-access-control/manifests/complex-admin-api.yaml b/api-management/2-access-control/manifests/complex-admin-api.yaml index a8b7c54..53c77a8 100644 --- a/api-management/2-access-control/manifests/complex-admin-api.yaml +++ b/api-management/2-access-control/manifests/complex-admin-api.yaml @@ -30,9 +30,10 @@ spec: entryPoints: - web routes: - - match: Host(`api.access-control.apimanagement.docker.localhost`) && Path(`/complex/admin`) + - match: Host(`api.access-control.apimanagement.docker.localhost`) && PathPrefix(`/complex/admin`) kind: Rule services: - name: admin-app port: 3000 - + middlewares: + - name: stripprefix-admin diff --git a/api-management/2-access-control/manifests/complex-weather-api.yaml b/api-management/2-access-control/manifests/complex-weather-api.yaml index bda5c8d..cb2e77b 100644 --- a/api-management/2-access-control/manifests/complex-weather-api.yaml +++ b/api-management/2-access-control/manifests/complex-weather-api.yaml @@ -12,11 +12,11 @@ spec: operationSets: - name: get-forecast matchers: - - pathPrefix: "/complex/weather" + - pathPrefix: "/weather" methods: [ "GET" ] - name: patch-forecast matchers: - - pathPrefix: "/complex/weather" + - pathPrefix: "/weather/0" methods: [ "PATCH" ] --- @@ -69,4 +69,5 @@ spec: services: - name: weather-app port: 3000 - + middlewares: + - name: stripprefix-weather diff --git a/api-management/2-access-control/manifests/simple-admin-api.yaml b/api-management/2-access-control/manifests/simple-admin-api.yaml index 97a2af6..1d64615 100644 --- a/api-management/2-access-control/manifests/simple-admin-api.yaml +++ b/api-management/2-access-control/manifests/simple-admin-api.yaml @@ -30,9 +30,10 @@ spec: entryPoints: - web routes: - - match: Host(`api.access-control.apimanagement.docker.localhost`) && Path(`/simple/admin`) + - match: Host(`api.access-control.apimanagement.docker.localhost`) && PathPrefix(`/simple/admin`) kind: Rule services: - name: admin-app port: 3000 - + middlewares: + - name: stripprefix-admin diff --git a/api-management/2-access-control/manifests/simple-weather-api.yaml b/api-management/2-access-control/manifests/simple-weather-api.yaml index b19b911..0f3103a 100644 --- a/api-management/2-access-control/manifests/simple-weather-api.yaml +++ b/api-management/2-access-control/manifests/simple-weather-api.yaml @@ -40,3 +40,5 @@ spec: services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather diff --git a/api-management/3-api-lifecycle-management/README.md b/api-management/3-api-lifecycle-management/README.md index 1c47b47..3f20dfd 100644 --- a/api-management/3-api-lifecycle-management/README.md +++ b/api-management/3-api-lifecycle-management/README.md @@ -15,6 +15,7 @@ kubectl apply -f api-management/3-api-lifecycle-management/manifests/api.yaml ```shell namespace/apps unchanged configmap/weather-data unchanged +middleware.traefik.io/stripprefix-weather unchanged deployment.apps/weather-app unchanged service/weather-app unchanged configmap/weather-app-openapispec unchanged @@ -47,7 +48,7 @@ To use API Version features, we'll need to: ```diff :../../hack/diff.sh -r -a "manifests/api.yaml manifests/api-v1.yaml" --- manifests/api.yaml +++ manifests/api-v1.yaml -@@ -1,10 +1,11 @@ +@@ -1,41 +1,54 @@ --- apiVersion: hub.traefik.io/v1alpha1 -kind: API @@ -61,10 +62,12 @@ To use API Version features, we'll need to: openApiSpec: path: /openapi.yaml override: -@@ -13,28 +14,38 @@ - - --- - apiVersion: hub.traefik.io/v1alpha1 + servers: +- - url: http://api.lifecycle.apimanagement.docker.localhost ++ - url: http://api.lifecycle.apimanagement.docker.localhost/weather-v1 ++ ++--- ++apiVersion: hub.traefik.io/v1alpha1 +kind: API +metadata: + name: api-lifecycle-apimanagement-weather-api-v1 @@ -72,9 +75,9 @@ To use API Version features, we'll need to: +spec: + versions: + - name: api-lifecycle-apimanagement-weather-api-v1 -+ -+--- -+apiVersion: hub.traefik.io/v1alpha1 + + --- + apiVersion: hub.traefik.io/v1alpha1 kind: APIAccess metadata: - name: api-lifecycle-apimanagement-weather-api @@ -100,11 +103,14 @@ To use API Version features, we'll need to: entryPoints: - web routes: -- - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather(/([0-9]+|openapi.yaml))?$`) -+ - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-v1(/([0-9]+|openapi.yaml))?$`) +- - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather`) ++ - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-v1`) kind: Rule services: - name: weather-app + port: 3000 ++ middlewares: ++ - name: stripprefix-weather ``` We can apply it: @@ -124,9 +130,9 @@ And confirm it's still working: ```shell # This call is not allowed -curl -i http://api.lifecycle.apimanagement.docker.localhost/weather-v1 +curl -i http://api.lifecycle.apimanagement.docker.localhost/weather-v1/weather # This call is allowed -curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-v1 +curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-v1/weather ``` ## Publish a second API Version @@ -143,7 +149,7 @@ So, for this second API Version, we'll need to: ```diff :../../hack/diff.sh -r -a "manifests/api-v1.yaml manifests/api-v1.1.yaml" --- manifests/api-v1.yaml +++ manifests/api-v1.1.yaml -@@ -2,10 +2,10 @@ +@@ -2,42 +2,43 @@ apiVersion: hub.traefik.io/v1alpha1 kind: APIVersion metadata: @@ -156,7 +162,11 @@ So, for this second API Version, we'll need to: openApiSpec: path: /openapi.yaml override: -@@ -16,28 +16,29 @@ + servers: +- - url: http://api.lifecycle.apimanagement.docker.localhost/weather-v1 ++ - url: http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions + + --- apiVersion: hub.traefik.io/v1alpha1 kind: API metadata: @@ -190,16 +200,18 @@ So, for this second API Version, we'll need to: namespace: apps annotations: hub.traefik.io/api-version: api-lifecycle-apimanagement-weather-api-v1 -@@ -45,8 +46,26 @@ +@@ -45,10 +46,30 @@ entryPoints: - web routes: -- - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-v1(/([0-9]+|openapi.yaml))?$`) -+ - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-multi-versions(/([0-9]+|openapi.yaml))?$`) +- - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-v1`) ++ - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-multi-versions`) kind: Rule services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather + +--- +apiVersion: traefik.io/v1alpha1 @@ -213,11 +225,13 @@ So, for this second API Version, we'll need to: + entryPoints: + - web + routes: -+ - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-multi-versions(/([0-9]+|openapi.yaml))?$`) && Header(`X-Version`, `preview`) ++ - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-multi-versions`) && Header(`X-Version`, `preview`) + kind: Rule + services: + - name: weather-app-forecast + port: 3000 ++ middlewares: ++ - name: stripprefix-weather ``` So let's do it: @@ -242,11 +256,11 @@ Now, we can test if it works: ```shell # Even with preview X-Version header, it should return 401 without token -curl -i -H "X-Version: preview" http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions +curl -i -H "X-Version: preview" http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions/weather # Regular access => returns weather data -curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions +curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions/weather # Preview access, with special header => returns forecast data -curl -H "X-Version: preview" -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions +curl -H "X-Version: preview" -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions/weather ``` To go further, one can use this pattern with other Traefik Middlewares to route versions based on many parameters: path, query, content type, clientIP, basicAuth, forwardAuth, and many others! @@ -266,7 +280,7 @@ Since the last step, the diff is looking like this: ```diff :../../hack/diff.sh -r -a "manifests/api-v1.1.yaml manifests/api-v1.1-weighted.yaml" --- manifests/api-v1.1.yaml +++ manifests/api-v1.1-weighted.yaml -@@ -1,62 +1,24 @@ +@@ -1,64 +1,24 @@ --- -apiVersion: hub.traefik.io/v1alpha1 -kind: APIVersion @@ -279,7 +293,7 @@ Since the last step, the diff is looking like this: - path: /openapi.yaml - override: - servers: -- - url: http://api.getting-started.apimanagement.docker.localhost +- - url: http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions - ---- -apiVersion: hub.traefik.io/v1alpha1 @@ -317,12 +331,14 @@ Since the last step, the diff is looking like this: - entryPoints: - - web - routes: -- - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-multi-versions(/([0-9]+|openapi.yaml))?$`) +- - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-multi-versions`) - kind: Rule + weighted: services: - - name: weather-app - port: 3000 +- middlewares: +- - name: stripprefix-weather + - name: weather-app + port: 3000 + weight: 1 @@ -339,18 +355,20 @@ Since the last step, the diff is looking like this: namespace: apps annotations: hub.traefik.io/api-version: api-lifecycle-apimanagement-weather-api-v1-1 -@@ -64,8 +26,9 @@ +@@ -66,10 +26,11 @@ entryPoints: - web routes: -- - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-multi-versions(/([0-9]+|openapi.yaml))?$`) && Header(`X-Version`, `preview`) -+ - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-v1-wrr(/([0-9]+|openapi.yaml))?$`) +- - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-multi-versions`) && Header(`X-Version`, `preview`) ++ - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-v1-wrr`) kind: Rule services: - - name: weather-app-forecast + - name: api-lifecycle-apimanagement-weather-api-wrr port: 3000 + kind: TraefikService + middlewares: + - name: stripprefix-weather ``` Let's apply it: @@ -367,10 +385,10 @@ ingressroute.traefik.io/weather-api created A simple test should confirm that it works: ```shell -curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-v1-wrr -curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-v1-wrr -curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-v1-wrr -curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-v1-wrr +curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-v1-wrr/weather +curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-v1-wrr/weather +curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-v1-wrr/weather +curl -H "Authorization: Bearer $ADMIN_TOKEN" http://api.lifecycle.apimanagement.docker.localhost/weather-v1-wrr/weather ``` To go further, it's also possible to mirror production traffic to a new version and/or to use a sticky session. diff --git a/api-management/3-api-lifecycle-management/manifests/api-portal.yaml b/api-management/3-api-lifecycle-management/manifests/api-portal.yaml new file mode 100644 index 0000000..433b252 --- /dev/null +++ b/api-management/3-api-lifecycle-management/manifests/api-portal.yaml @@ -0,0 +1,33 @@ +--- +apiVersion: hub.traefik.io/v1alpha1 +kind: APIPortal +metadata: + name: api-lifecycle-apimanagement-apiportal + namespace: apps +spec: + title: API Portal + description: "Apps Developer Portal" + trustedUrls: + - http://api.lifecycle.apimanagement.docker.localhost + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: api-lifecycle-apimanagement-apiportal + namespace: traefik + annotations: + # This annotation link this Ingress to the API Portal using @ format. + hub.traefik.io/api-portal: api-lifecycle-apimanagement-apiportal@apps +spec: + rules: + - host: api.lifecycle.apimanagement.docker.localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: apiportal + port: + number: 9903 diff --git a/api-management/3-api-lifecycle-management/manifests/api-v1.1-weighted.yaml b/api-management/3-api-lifecycle-management/manifests/api-v1.1-weighted.yaml index 70cf1de..5744dfe 100644 --- a/api-management/3-api-lifecycle-management/manifests/api-v1.1-weighted.yaml +++ b/api-management/3-api-lifecycle-management/manifests/api-v1.1-weighted.yaml @@ -26,9 +26,11 @@ spec: entryPoints: - web routes: - - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-v1-wrr(/([0-9]+|openapi.yaml))?$`) + - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-v1-wrr`) kind: Rule services: - name: api-lifecycle-apimanagement-weather-api-wrr port: 3000 kind: TraefikService + middlewares: + - name: stripprefix-weather diff --git a/api-management/3-api-lifecycle-management/manifests/api-v1.1.yaml b/api-management/3-api-lifecycle-management/manifests/api-v1.1.yaml index 32a72ce..75ac352 100644 --- a/api-management/3-api-lifecycle-management/manifests/api-v1.1.yaml +++ b/api-management/3-api-lifecycle-management/manifests/api-v1.1.yaml @@ -10,7 +10,7 @@ spec: path: /openapi.yaml override: servers: - - url: http://api.getting-started.apimanagement.docker.localhost + - url: http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions --- apiVersion: hub.traefik.io/v1alpha1 @@ -46,11 +46,13 @@ spec: entryPoints: - web routes: - - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-multi-versions(/([0-9]+|openapi.yaml))?$`) + - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-multi-versions`) kind: Rule services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather --- apiVersion: traefik.io/v1alpha1 @@ -64,8 +66,10 @@ spec: entryPoints: - web routes: - - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-multi-versions(/([0-9]+|openapi.yaml))?$`) && Header(`X-Version`, `preview`) + - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-multi-versions`) && Header(`X-Version`, `preview`) kind: Rule services: - name: weather-app-forecast port: 3000 + middlewares: + - name: stripprefix-weather diff --git a/api-management/3-api-lifecycle-management/manifests/api-v1.yaml b/api-management/3-api-lifecycle-management/manifests/api-v1.yaml index 31ac983..e64e732 100644 --- a/api-management/3-api-lifecycle-management/manifests/api-v1.yaml +++ b/api-management/3-api-lifecycle-management/manifests/api-v1.yaml @@ -10,7 +10,7 @@ spec: path: /openapi.yaml override: servers: - - url: http://api.getting-started.apimanagement.docker.localhost + - url: http://api.lifecycle.apimanagement.docker.localhost/weather-v1 --- apiVersion: hub.traefik.io/v1alpha1 @@ -45,8 +45,10 @@ spec: entryPoints: - web routes: - - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather-v1(/([0-9]+|openapi.yaml))?$`) + - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather-v1`) kind: Rule services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather diff --git a/api-management/3-api-lifecycle-management/manifests/api.yaml b/api-management/3-api-lifecycle-management/manifests/api.yaml index d5be661..656818e 100644 --- a/api-management/3-api-lifecycle-management/manifests/api.yaml +++ b/api-management/3-api-lifecycle-management/manifests/api.yaml @@ -9,7 +9,7 @@ spec: path: /openapi.yaml override: servers: - - url: http://api.getting-started.apimanagement.docker.localhost + - url: http://api.lifecycle.apimanagement.docker.localhost --- apiVersion: hub.traefik.io/v1alpha1 @@ -34,7 +34,7 @@ spec: entryPoints: - web routes: - - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathRegexp(`^/weather(/([0-9]+|openapi.yaml))?$`) + - match: Host(`api.lifecycle.apimanagement.docker.localhost`) && PathPrefix(`/weather`) kind: Rule services: - name: weather-app diff --git a/src/manifests/admin-app.yaml b/src/manifests/admin-app.yaml index ab08079..9fb293a 100644 --- a/src/manifests/admin-app.yaml +++ b/src/manifests/admin-app.yaml @@ -13,12 +13,28 @@ metadata: data: api.json: | { - "settings": [ - { "id": 1, "lang": "en" }, - { "id": 2, "lang": "fr" }, - ] + "admin": { + "settings": { + "1": { + "lang": "en"}, + "2": { + "lang": "fr"} + } + } } +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: stripprefix-admin + namespace: admin +spec: + stripPrefix: + prefixes: + - /simple + - /complex + --- apiVersion: apps/v1 kind: Deployment @@ -37,9 +53,9 @@ spec: spec: containers: - name: api - image: ghcr.io/traefik-workshops/api-server:v0.2.0 + image: ghcr.io/traefik/api-server:v1.0.0 args: ["-data", "/api/api.json", "-errorrate", "2"] - imagePullPolicy: Always + imagePullPolicy: IfNotPresent volumeMounts: - name: api-data mountPath: /api diff --git a/src/manifests/walkthrough/api.yaml b/src/manifests/walkthrough/api.yaml index 8718da9..bb06af9 100644 --- a/src/manifests/walkthrough/api.yaml +++ b/src/manifests/walkthrough/api.yaml @@ -9,7 +9,7 @@ spec: path: /openapi.yaml override: servers: - - url: http://api.getting-started.apimanagement.docker.localhost + - url: http://api.walkthrough.docker.localhost --- apiVersion: hub.traefik.io/v1alpha1 @@ -34,8 +34,10 @@ spec: entryPoints: - web routes: - - match: Host(`api.walkthrough.docker.localhost`) && PathRegexp(`^/weather(/([0-9]+|openapi.yaml))?$`) + - match: Host(`api.walkthrough.docker.localhost`) && PathPrefix(`/weather`) kind: Rule services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather diff --git a/src/manifests/walkthrough/forecast.yaml b/src/manifests/walkthrough/forecast.yaml index 5d6e043..48c1283 100644 --- a/src/manifests/walkthrough/forecast.yaml +++ b/src/manifests/walkthrough/forecast.yaml @@ -29,8 +29,10 @@ spec: entryPoints: - web routes: - - match: Host(`api.walkthrough.docker.localhost`) && Path(`/forecast`) + - match: Host(`api.walkthrough.docker.localhost`) && PathPrefix(`/forecast`) kind: Rule services: - name: weather-app-forecast port: 3000 + middlewares: + - name: stripprefix-weather diff --git a/src/manifests/walkthrough/weather-app-apikey.yaml b/src/manifests/walkthrough/weather-app-apikey.yaml index 692ccc5..f3a266f 100644 --- a/src/manifests/walkthrough/weather-app-apikey.yaml +++ b/src/manifests/walkthrough/weather-app-apikey.yaml @@ -32,10 +32,11 @@ spec: entryPoints: - web routes: - - match: Host(`walkthrough.docker.localhost`) && Path(`/api-key`) + - match: Host(`walkthrough.docker.localhost`) && PathPrefix(`/api-key`) kind: Rule services: - name: weather-app port: 3000 middlewares: - - name: walkthrough-apikey-auth + - name: stripprefix-weather + - name: walkthrough-apikey-auth diff --git a/src/manifests/walkthrough/weather-app-basic-auth.yaml b/src/manifests/walkthrough/weather-app-basic-auth.yaml index 53654b3..0db0be2 100644 --- a/src/manifests/walkthrough/weather-app-basic-auth.yaml +++ b/src/manifests/walkthrough/weather-app-basic-auth.yaml @@ -28,10 +28,11 @@ spec: entryPoints: - web routes: - - match: Host(`walkthrough.docker.localhost`) && Path(`/basic-auth`) + - match: Host(`walkthrough.docker.localhost`) && PathPrefix(`/basic-auth`) kind: Rule services: - name: weather-app port: 3000 middlewares: - - name: basic-auth + - name: stripprefix-weather + - name: basic-auth diff --git a/src/manifests/walkthrough/weather-app-no-auth.yaml b/src/manifests/walkthrough/weather-app-no-auth.yaml index 33cef76..f4c4de0 100644 --- a/src/manifests/walkthrough/weather-app-no-auth.yaml +++ b/src/manifests/walkthrough/weather-app-no-auth.yaml @@ -8,8 +8,10 @@ spec: entryPoints: - web routes: - - match: Host(`walkthrough.docker.localhost`) && Path(`/no-auth`) + - match: Host(`walkthrough.docker.localhost`) && PathPrefix(`/no-auth`) kind: Rule services: - name: weather-app port: 3000 + middlewares: + - name: stripprefix-weather diff --git a/src/manifests/weather-app-forecast.yaml b/src/manifests/weather-app-forecast.yaml index f37d490..509acac 100644 --- a/src/manifests/weather-app-forecast.yaml +++ b/src/manifests/weather-app-forecast.yaml @@ -7,11 +7,11 @@ metadata: data: api.json: | { - "forecast": [ - { "id": 1, "city": "GopherCity", "weather": "Cloudy", "dt": "3128231402" }, - { "id": 2, "city": "City of Gopher", "weather": "Rainy", "dt": "3128231402" }, - { "id": 3, "code": "GopherCentral", "weather": "Shiny", "dt": "3128231402" } - ] + "weather": { + "1": { "city": "GopherCity", "weather": "Cloudy", "dt": "3128231402" }, + "2": { "city": "City of Gopher", "weather": "Rainy", "dt": "3128231402" }, + "3": { "code": "GopherCentral", "weather": "Shiny", "dt": "3128231402" } + } } --- @@ -32,9 +32,9 @@ spec: spec: containers: - name: api - image: ghcr.io/traefik-workshops/api-server:v0.2.0 + image: ghcr.io/traefik/api-server:v1.0.0 args: ["-data", "/api/api.json", "-errorrate", "2"] - imagePullPolicy: Always + imagePullPolicy: IfNotPresent volumeMounts: - name: api-data mountPath: /api diff --git a/src/manifests/weather-app.yaml b/src/manifests/weather-app.yaml index 6ee5603..2a1d40d 100644 --- a/src/manifests/weather-app.yaml +++ b/src/manifests/weather-app.yaml @@ -7,13 +7,33 @@ metadata: data: api.json: | { - "weather": [ - { "id": 0, "city": "GopherCity", "weather": "Moderate rain" }, - { "id": 1, "city": "City of Gophers", "weather": "Sunny" }, - { "id": 2, "city": "GopherRocks", "weather": "Cloudy" } - ] + "weather": { + "0": {"city": "GopherCity", "weather": "Moderate rain"}, + "1": {"city": "City of Gophers", "weather": "Sunny"}, + "2": {"city": "GopherRocks", "weather": "Cloudy"} + } } +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: stripprefix-weather + namespace: apps +spec: + stripPrefix: + prefixes: + - /api-key + - /simple + - /complex + - /weather-v1-wrr + - /weather-v1 + - /weather-multi-versions + - /no-auth + - /basic-auth + - /api-key + - /forecast + --- apiVersion: apps/v1 kind: Deployment @@ -32,9 +52,9 @@ spec: spec: containers: - name: api - image: ghcr.io/traefik-workshops/api-server:v0.2.0 + image: ghcr.io/traefik/api-server:v1.0.0 args: ["-data", "/api/api.json", "-openapi", "/public/openapi.yaml", "-errorrate", "2"] - imagePullPolicy: Always + imagePullPolicy: IfNotPresent volumeMounts: - name: api-data mountPath: /api @@ -91,37 +111,53 @@ data: paths: /weather: get: - summary: Retrieve all registered weather - operationId: listWeather + summary: Retrieve all registered weather of all cities + operationId: getAll tags: - external responses: '200': - description: An array of weathers + description: An array of weather data + content: + application/json: + schema: + $ref: "#/components/schemas/weather" + '404': + $ref: '#/components/responses/notFound' + '500': + $ref: '#/components/responses/serverError' + post: + summary: Create a weather record + operationId: post + tags: + - external + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/weatherWithoutId' + responses: + '201': + description: The created weather with its id content: application/json: schema: $ref: "#/components/schemas/weather" - '204': - $ref: '#/components/responses/nocontent' - '401': - $ref: '#/components/responses/unauthorized' '500': $ref: '#/components/responses/serverError' /weather/{id}: get: summary: Retrieve weather of a city - operationId: getWeather + operationId: get tags: - external parameters: - - name: id - in: path - description: Record ID - required: true - schema: - type: integer - format: int64 + - name: id + in: path + description: Record ID + required: true + schema: + type: string responses: '200': description: A weather @@ -129,15 +165,28 @@ data: application/json: schema: $ref: "#/components/schemas/weather" - '204': - $ref: '#/components/responses/nocontent' - '401': - $ref: '#/components/responses/unauthorized' + '404': + $ref: '#/components/responses/notFound' '500': $ref: '#/components/responses/serverError' - patch: - summary: Update weather data - operationId: patchForecast + delete: + summary: Delete weather of a city + operationId: delete + tags: + - external + parameters: + - name: id + in: path + description: Record ID + required: true + schema: + type: string + responses: + '204': + $ref: '#/components/responses/noContent' + put: + summary: Update weather of a city + operationId: put tags: - external parameters: @@ -146,32 +195,54 @@ data: description: Record ID required: true schema: - type: integer - format: int64 - - name: city - in: query - description: City + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/weatherWithoutId' + responses: + '200': + $ref: '#/components/responses/noContent' + patch: + summary: Patch weather data + operationId: patch + tags: + - external + parameters: + - name: id + in: path + description: Record ID required: true schema: type: string + requestBody: + content: + application/json-patch+json: + schema: + $ref: '#/components/schemas/patchRequest' responses: '200': - description: An array of forecasted weathers + description: An array of forecasted weather data content: application/json: schema: $ref: "#/components/schemas/weather" '204': - $ref: '#/components/responses/nocontent' + $ref: '#/components/responses/noContent' '401': $ref: '#/components/responses/unauthorized' '500': $ref: '#/components/responses/serverError' - + components: responses: - nocontent: + created: + description: "Created" + noContent: description: "No content" + notFound: + description: "Not found" unauthorized: description: "Access token is missing or invalid" serverError: @@ -184,8 +255,16 @@ data: - name properties: id: - type: integer - format: int64 + type: string + city: + type: string + weather: + type: string + weatherWithoutId: + type: object + required: + - name + properties: city: type: string weather: @@ -195,6 +274,68 @@ data: maxItems: 100 items: $ref: "#/components/schemas/weather" + patchRequest: + type: array + items: + oneOf: + - $ref: '#/components/schemas/JSONPatchRequestAddReplaceTest' + - $ref: '#/components/schemas/JSONPatchRequestRemove' + - $ref: '#/components/schemas/JSONPatchRequestMoveCopy' + JSONPatchRequestAddReplaceTest: + type: object + additionalProperties: false + required: + - value + - op + - path + properties: + path: + description: A JSON Pointer path. + type: string + value: + description: The value to add, replace or test. + op: + description: The operation to perform. + type: string + enum: + - add + - replace + - test + JSONPatchRequestRemove: + type: object + additionalProperties: false + required: + - op + - path + properties: + path: + description: A JSON Pointer path. + type: string + op: + description: The operation to perform. + type: string + enum: + - remove + JSONPatchRequestMoveCopy: + type: object + additionalProperties: false + required: + - from + - op + - path + properties: + path: + description: A JSON Pointer path. + type: string + op: + description: The operation to perform. + type: string + enum: + - move + - copy + from: + description: A JSON Pointer path. + type: string securitySchemes: bearerAuth: description: "Bearer Auth" diff --git a/src/manifests/weather-appv2.yaml b/src/manifests/weather-appv2.yaml index e09a2a1..90b2eba 100644 --- a/src/manifests/weather-appv2.yaml +++ b/src/manifests/weather-appv2.yaml @@ -7,16 +7,16 @@ metadata: data: api.json: | { - "forecast": [ - { "id": 1, "city": "GopherCity", "weather": "Cloudy", "dt": "3128231402" }, - { "id": 2, "city": "City of Gopher", "weather": "Rainy", "dt": "3128231402" }, - { "id": 3, "code": "GopherCentral", "weather": "Shiny", "dt": "3128231402" } - ], - "forecast-fr": [ - { "id": 1, "city": "GopherCity", "weather": "Nuageux", "dt": "3128231402" }, - { "id": 2, "city": "City of Gopher", "weather": "Pluvieux", "dt": "3128231402" }, - { "id": 3, "code": "GopherCentral", "weather": "Ensoleillé", "dt": "3128231402" } - ] + "forecast": { + "1": {"city": "GopherCity", "weather": "Cloudy", "dt": "3128231402"}, + "2": {"city": "City of Gopher", "weather": "Rainy", "dt": "3128231402"}, + "3": {"code": "GopherCentral", "weather": "Shiny", "dt": "3128231402"} + }, + "forecast-fr": { + "1": {"city": "GopherCity", "weather": "Nuageux", "dt": "3128231402"}, + "2": {"city": "City of Gopher", "weather": "Pluvieux", "dt": "3128231402"}, + "3": {"code": "GopherCentral", "weather": "Ensoleillé", "dt": "3128231402"} + } } --- apiVersion: apps/v1 @@ -36,9 +36,9 @@ spec: spec: containers: - name: api - image: ghcr.io/traefik-workshops/api-server:v0.2.0 + image: ghcr.io/traefik/api-server:v1.0.0 args: ["-data", "/api/api.json", "-openapi", "/public/openapi.yaml", "-errorrate", "2"] - imagePullPolicy: Always + imagePullPolicy: IfNotPresent volumeMounts: - name: api-data mountPath: /api diff --git a/tests/apigateway/apigateway_test.go b/tests/apigateway/apigateway_test.go index 3c5bef4..8eb1bc6 100644 --- a/tests/apigateway/apigateway_test.go +++ b/tests/apigateway/apigateway_test.go @@ -108,13 +108,13 @@ func (s *APIGatewayTestSuite) TestGettingStarted() { s.Require().NoError(err) s.apply("api-gateway/1-getting-started/manifests/weather-app-ingressroute.yaml") - req, err := http.NewRequest(http.MethodGet, "http://getting-started.apigateway.docker.localhost", nil) + req, err := http.NewRequest(http.MethodGet, "http://getting-started.apigateway.docker.localhost/weather", nil) s.Require().NoError(err) err = try.RequestWithTransport(req, 30*time.Second, s.tr, try.StatusCodeIs(http.StatusOK)) s.Assert().NoError(err) s.apply("api-gateway/1-getting-started/manifests/weather-app-apikey.yaml") - req, err = http.NewRequest(http.MethodGet, "http://getting-started.apigateway.docker.localhost/api-key", nil) + req, err = http.NewRequest(http.MethodGet, "http://getting-started.apigateway.docker.localhost/api-key/weather", nil) s.Require().NoError(err) err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.StatusCodeIs(http.StatusUnauthorized)) s.Assert().NoError(err) diff --git a/tests/apimanagement/apimanagement_test.go b/tests/apimanagement/apimanagement_test.go index a7da930..bfe9e13 100644 --- a/tests/apimanagement/apimanagement_test.go +++ b/tests/apimanagement/apimanagement_test.go @@ -1,8 +1,10 @@ package apimanagement import ( + "bytes" "context" "errors" + "io" "net" "net/http" "os" @@ -106,7 +108,7 @@ func (s *APIManagementTestSuite) TestGettingStarted() { err = s.apply("api-management/1-getting-started/manifests/weather-app-ingressroute.yaml") s.Assert().NoError(err) - err = s.check(http.MethodGet, "http://getting-started.apimanagement.docker.localhost", 10*time.Second, http.StatusOK) + err = s.check(http.MethodGet, "http://getting-started.apimanagement.docker.localhost/weather", 10*time.Second, http.StatusOK) s.Assert().NoError(err) err = s.apply("api-management/1-getting-started/manifests/api.yaml") @@ -119,7 +121,7 @@ func (s *APIManagementTestSuite) TestGettingStarted() { err = s.check(http.MethodGet, "http://api.getting-started.apimanagement.docker.localhost", 90*time.Second, http.StatusOK) s.Assert().NoError(err) - err = s.checkWithBearer(http.MethodGet, "http://api.getting-started.apimanagement.docker.localhost/weather", adminToken, 90*time.Second, http.StatusOK) + err = s.checkWithBearer(http.MethodGet, "http://api.getting-started.apimanagement.docker.localhost/weather", http.NoBody, adminToken, 90*time.Second, http.StatusOK) s.Assert().NoError(err) } @@ -145,16 +147,16 @@ func (s *APIManagementTestSuite) TestAccessControl() { err = s.apply("api-management/2-access-control/manifests/simple-weather-api.yaml") s.Require().NoError(err) - err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/simple/admin", adminToken, 90*time.Second, http.StatusOK) + err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/simple/admin", http.NoBody, adminToken, 90*time.Second, http.StatusOK) s.Assert().NoError(err) - err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/simple/weather", adminToken, 5*time.Second, http.StatusForbidden) + err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/simple/weather", http.NoBody, adminToken, 5*time.Second, http.StatusForbidden) s.Assert().NoError(err) - err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/simple/weather", externalToken, 5*time.Second, http.StatusOK) + err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/simple/weather", http.NoBody, externalToken, 5*time.Second, http.StatusOK) s.Assert().NoError(err) - err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/simple/admin", externalToken, 5*time.Second, http.StatusForbidden) + err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/simple/admin", http.NoBody, externalToken, 5*time.Second, http.StatusForbidden) s.Assert().NoError(err) // Complex Access Control @@ -163,25 +165,25 @@ func (s *APIManagementTestSuite) TestAccessControl() { err = s.apply("api-management/2-access-control/manifests/complex-weather-api.yaml") s.Require().NoError(err) - err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/complex/admin", adminToken, 10*time.Second, http.StatusOK) + err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/complex/admin", http.NoBody, adminToken, 10*time.Second, http.StatusOK) s.Assert().NoError(err) - err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/complex/weather", adminToken, 5*time.Second, http.StatusOK) + err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/complex/weather", http.NoBody, adminToken, 5*time.Second, http.StatusOK) s.Assert().NoError(err) - err = s.checkWithBearer(http.MethodPatch, "http://api.access-control.apimanagement.docker.localhost/complex/weather", adminToken, 5*time.Second, http.StatusOK) + err = s.checkWithBearer(http.MethodPatch, "http://api.access-control.apimanagement.docker.localhost/complex/weather/0", bytes.NewReader([]byte(`[{"op": "replace", "path": "/city", "value": "GopherTown"}]`)), adminToken, 5*time.Second, http.StatusNoContent) s.Assert().NoError(err) - err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/complex/weather", externalToken, 5*time.Second, http.StatusOK) + err = s.checkWithBearer(http.MethodGet, "http://api.access-control.apimanagement.docker.localhost/complex/weather", http.NoBody, externalToken, 5*time.Second, http.StatusOK) s.Assert().NoError(err) - err = s.checkWithBearer(http.MethodPatch, "http://api.access-control.apimanagement.docker.localhost/complex/weather", externalToken, 5*time.Second, http.StatusOK) + err = s.checkWithBearer(http.MethodPatch, "http://api.access-control.apimanagement.docker.localhost/complex/weather/0", bytes.NewReader([]byte(`[{"op": "replace", "path": "/weather", "value": "Cloudy"}]`)), externalToken, 5*time.Second, http.StatusNoContent) s.Assert().NoError(err) err = testhelpers.Delete(s.ctx, s.k8s, "APIAccess", "access-control-apimanagement-simple-weather", "apps", "hub.traefik.io", "v1alpha1") s.Require().NoError(err) - err = s.checkWithBearer(http.MethodPatch, "http://api.access-control.apimanagement.docker.localhost/complex/weather", externalToken, 10*time.Second, http.StatusForbidden) + err = s.checkWithBearer(http.MethodPatch, "http://api.access-control.apimanagement.docker.localhost/complex/weather/0", bytes.NewReader([]byte(`[{"op": "replace", "path": "/weather", "value": "Cloudy"}]`)), externalToken, 10*time.Second, http.StatusForbidden) s.Assert().NoError(err) } @@ -203,16 +205,16 @@ func (s *APIManagementTestSuite) TestAPILifeCycleManagement() { err = s.check(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather", 5*time.Second, http.StatusUnauthorized) s.Assert().NoError(err) - err = s.checkWithBearer(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather", adminToken, 90*time.Second, http.StatusOK) + err = s.checkWithBearer(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather", http.NoBody, adminToken, 90*time.Second, http.StatusOK) s.Assert().NoError(err) err = s.apply("api-management/3-api-lifecycle-management/manifests/api-v1.yaml") s.Require().NoError(err) time.Sleep(1 * time.Second) - err = s.check(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-v1", 5*time.Second, http.StatusUnauthorized) + err = s.check(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-v1/weather", 5*time.Second, http.StatusUnauthorized) s.Assert().NoError(err) - err = s.checkWithBearer(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-v1", adminToken, 90*time.Second, http.StatusOK) + err = s.checkWithBearer(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-v1/weather", http.NoBody, adminToken, 90*time.Second, http.StatusOK) s.Assert().NoError(err) // Publish Second API Version @@ -226,23 +228,23 @@ func (s *APIManagementTestSuite) TestAPILifeCycleManagement() { s.Require().NoError(err) var req *http.Request - req, err = http.NewRequest(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions", nil) + req, err = http.NewRequest(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions/weather", nil) s.Require().NoError(err) req.Header.Add("X-Version", "preview") err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.StatusCodeIs(http.StatusUnauthorized)) s.Assert().NoError(err) - req, err = http.NewRequest(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions", nil) + req, err = http.NewRequest(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions/weather", nil) s.Require().NoError(err) req.Header.Add("Authorization", "Bearer "+adminToken) - err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.BodyContains("weather")) + err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.BodyContains("GopherCity")) s.Assert().NoError(err) - req, err = http.NewRequest(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions", nil) + req, err = http.NewRequest(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-multi-versions/weather", nil) s.Require().NoError(err) req.Header.Add("X-Version", "preview") req.Header.Add("Authorization", "Bearer "+adminToken) - err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.BodyContains("forecast")) + err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.BodyContains("GopherRocks")) s.Assert().NoError(err) // Try the new version with a part of the traffic @@ -250,9 +252,12 @@ func (s *APIManagementTestSuite) TestAPILifeCycleManagement() { s.Require().NoError(err) // both should work - err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.BodyContains("forecast")) + req, err = http.NewRequest(http.MethodGet, "http://api.lifecycle.apimanagement.docker.localhost/weather-v1-wrr/weather", nil) + s.Require().NoError(err) + req.Header.Add("Authorization", "Bearer "+adminToken) + err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.BodyContains("GopherRocks")) s.Assert().NoError(err) - err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.BodyContains("weather")) + err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.BodyContains("GopherCity")) s.Assert().NoError(err) } @@ -278,8 +283,8 @@ func (s *APIManagementTestSuite) check(method string, url string, timeout time.D return try.RequestWithTransport(req, timeout, s.tr, try.StatusCodeIs(status)) } -func (s *APIManagementTestSuite) checkWithBearer(method, url, bearer string, timeout time.Duration, status int) error { - req, err := http.NewRequest(method, url, nil) +func (s *APIManagementTestSuite) checkWithBearer(method, url string, body io.Reader, bearer string, timeout time.Duration, status int) error { + req, err := http.NewRequest(method, url, body) if err != nil { return err } diff --git a/tests/go.mod b/tests/go.mod index 1135353..c70b7e4 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -3,6 +3,7 @@ module github.com/traefik/hub/tests go 1.22.0 require ( + github.com/docker/docker v25.0.5+incompatible github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.31.0 @@ -25,7 +26,6 @@ require ( github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.5.0 // indirect - github.com/docker/docker v25.0.5+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect diff --git a/tests/testhelpers/containers.go b/tests/testhelpers/containers.go index 01caf09..d0da1b8 100644 --- a/tests/testhelpers/containers.go +++ b/tests/testhelpers/containers.go @@ -15,6 +15,7 @@ import ( "time" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -49,9 +50,9 @@ func CreateKubernetesCluster(ctx context.Context, t *testing.T) (*k3s.K3sContain Image: rancherImage, ExposedPorts: []string{"80/tcp", "443/tcp", "6443/tcp", "8443/tcp"}, HostConfigModifier: func(hc *container.HostConfig) { - hc.NetworkMode = "host" + hc.NetworkMode = network.NetworkBridge + hc.Privileged = true }, - Privileged: true, }, }), ) diff --git a/tests/walkthrough/walkthrough_test.go b/tests/walkthrough/walkthrough_test.go index 5403ee1..16f9f7d 100644 --- a/tests/walkthrough/walkthrough_test.go +++ b/tests/walkthrough/walkthrough_test.go @@ -100,14 +100,14 @@ func (s *WalkthroughTestSuite) TestWalkthrough() { s.apply("src/manifests/weather-app.yaml") s.apply("src/manifests/walkthrough/weather-app-no-auth.yaml") - req, err := http.NewRequest(http.MethodGet, "http://walkthrough.docker.localhost/no-auth", nil) + req, err := http.NewRequest(http.MethodGet, "http://walkthrough.docker.localhost/no-auth/weather", nil) s.Require().NoError(err) err = try.RequestWithTransport(req, 10*time.Second, s.tr, try.StatusCodeIs(http.StatusOK)) s.Assert().NoError(err) s.apply("src/manifests/walkthrough/weather-app-basic-auth.yaml") - req, err = http.NewRequest(http.MethodGet, "http://walkthrough.docker.localhost/basic-auth", nil) + req, err = http.NewRequest(http.MethodGet, "http://walkthrough.docker.localhost/basic-auth/weather", nil) s.Require().NoError(err) err = try.RequestWithTransport(req, 5*time.Second, s.tr, try.StatusCodeIs(http.StatusUnauthorized)) @@ -127,7 +127,7 @@ func (s *WalkthroughTestSuite) TestWalkthrough() { "--set", "image.tag=v3.3.0", "traefik/traefik") - req, err = http.NewRequest(http.MethodGet, "http://walkthrough.docker.localhost/basic-auth", nil) + req, err = http.NewRequest(http.MethodGet, "http://walkthrough.docker.localhost/basic-auth/weather", nil) s.Require().NoError(err) err = try.RequestWithTransport(req, 5*time.Second, s.tr, try.StatusCodeIs(http.StatusUnauthorized)) @@ -137,7 +137,7 @@ func (s *WalkthroughTestSuite) TestWalkthrough() { s.Assert().NoError(err) s.apply("src/manifests/walkthrough/weather-app-apikey.yaml") - req, err = http.NewRequest(http.MethodGet, "http://walkthrough.docker.localhost/api-key", nil) + req, err = http.NewRequest(http.MethodGet, "http://walkthrough.docker.localhost/api-key/weather", nil) s.Require().NoError(err) err = try.RequestWithTransport(req, 5*time.Second, s.tr, try.StatusCodeIs(http.StatusUnauthorized)) s.Assert().NoError(err) @@ -153,7 +153,7 @@ func (s *WalkthroughTestSuite) TestWalkthrough() { "--set", "hub.apimanagement.enabled=true", "traefik/traefik") - req, err = http.NewRequest(http.MethodGet, "http://walkthrough.docker.localhost/api-key", nil) + req, err = http.NewRequest(http.MethodGet, "http://walkthrough.docker.localhost/api-key/weather", nil) s.Require().NoError(err) err = try.RequestWithTransport(req, 5*time.Second, s.tr, try.StatusCodeIs(http.StatusUnauthorized))