diff --git a/Dockerfile b/Dockerfile index a0ec3de..7df509a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,13 @@ -FROM nginx:alpine +FROM traefik:v2.9.5 LABEL author="Piotr Biernat" -LABEL service="api-gw" +LABEL service="api-gateway" LABEL vendor="Egommerce" LABEL version="1.0" -COPY ./data/etc/nginx/ /etc/nginx/ +COPY ./api-gateway/etc /etc/traefik +COPY ./api-gateway/plugins /plugins-local + +EXPOSE 443 8080 + +# ENTRYPOINT ["/traefik"] # FIXME stack->stack config.yml diff --git a/api-gateway/.vscode/launch.json b/api-gateway/.vscode/launch.json new file mode 100644 index 0000000..2403c85 --- /dev/null +++ b/api-gateway/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Debug App", + "request": "launch", + "mainClass": "com.egommerce.apigateway.Bootstrap", + "projectName": "api-gateway" + } + ] +} \ No newline at end of file diff --git a/api-gateway/.vscode/settings.json b/api-gateway/.vscode/settings.json new file mode 100644 index 0000000..89a7e88 --- /dev/null +++ b/api-gateway/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive", + "maven.view": "hierarchical" +} \ No newline at end of file diff --git a/api-gateway/etc/traefik.yml b/api-gateway/etc/traefik.yml new file mode 100644 index 0000000..78ea2fc --- /dev/null +++ b/api-gateway/etc/traefik.yml @@ -0,0 +1,76 @@ +################################################################ +# global: + # checkNewVersion: true + # sendAnonymousUsage: true + +################################################################ +entryPoints: + https: + address: :443 + transport: + respondingTimeouts: + readTimeout: '10ms' + writeTimeout: '10ms' + idleTimeout: '20ms' + db: + address: :5432 + mongodb: + address: :27017 + eventbus: + address: :5672 + # redis: + # address: :6379 + +################################################################ +serversTransport: +# # insecureSkipVerify: true + rootCAs: + - /etc/traefik/certs/identity-svc/server.cert + - /etc/traefik/certs/basket-svc/server.cert + - /etc/traefik/certs/catalog-svc/server.cert + - /etc/traefik/certs/postgres-db/server.cert + # - /etc/traefik/certs/api-eventbus/server.cert + +################################################################ +api: + insecure: true + # dashboard: true + +################################################################ +providers: + # file: + # directory: /etc/traefik/services +################################################################ + docker: + # Docker server endpoint. Can be a tcp or a unix socket endpoint. + # endpoint: unix:///var/run/docker.sock + exposedByDefault: false + # network: api-gateway-network + # useBindPortIP: true + + # Default host rule. + # Optional + # Default: "Host(`{{ normalize .Name }}`)" + # defaultRule: Host(`{{ normalize .Name }}.docker.localhost`) +################################################################ + consulCatalog: + exposedByDefault: false + # serviceName: api-gateway + refreshInterval: 30s + endpoint: + address: api-registry:8500 + +################################################################ +log: + level: DEBUG +# filePath: log/traefik.log +# format: json + +################################################################ +# accessLog: {} + +################################################################ +experimental: + localPlugins: + requestid: + moduleName: "git.pbiernat.dev/traefik/plugin-requestid" diff --git a/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/.traefik.yml b/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/.traefik.yml new file mode 100644 index 0000000..02fd233 --- /dev/null +++ b/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/.traefik.yml @@ -0,0 +1,6 @@ +displayName: Add X-Request-ID Header +type: middleware +import: git.pbiernat.dev/traefik/plugin-requestid +summary: 'Add a X-Request-ID header for tracing' + +testData: {} \ No newline at end of file diff --git a/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/README.md b/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/README.md new file mode 100644 index 0000000..1a9f7bd --- /dev/null +++ b/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/README.md @@ -0,0 +1,3 @@ +# plugin-requestid + +Add X-Request-ID header \ No newline at end of file diff --git a/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/go.mod b/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/go.mod new file mode 100644 index 0000000..d74eef9 --- /dev/null +++ b/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/go.mod @@ -0,0 +1,3 @@ +module git.pbiernat.dev/traefik/plugin-requestid + +go 1.18 diff --git a/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/requestid.go b/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/requestid.go new file mode 100644 index 0000000..d5833d6 --- /dev/null +++ b/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/requestid.go @@ -0,0 +1,61 @@ +package plugin_requestid + +import ( + "context" + "fmt" + "net/http" +) + +const defaultHeaderName = "X-Request-ID" + +// Config plugin configuration +type Config struct { + HeaderName string `json:"headerName"` +} + +// CreateConfig create default plugin configuration +func CreateConfig() *Config { + return &Config{ + HeaderName: defaultHeaderName, + } +} + +// RequestIDHeader +type RequestIDHeader struct { + headerName string + name string + next http.Handler +} + +// New create new RequestIDHeader +func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { + hdr := &RequestIDHeader{ + next: next, + name: name, + } + + if config == nil { + return nil, fmt.Errorf("config can not be nil") + } + + if config.HeaderName == "" { + hdr.headerName = defaultHeaderName + } else { + hdr.headerName = config.HeaderName + } + + return hdr, nil + +} + +func (r *RequestIDHeader) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + uuid := newUUID().String() + + // header injection to backend service + req.Header.Add(r.headerName, uuid) + + // header injection to client response + rw.Header().Add(r.headerName, uuid) + + r.next.ServeHTTP(rw, req) +} diff --git a/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/uuid.go b/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/uuid.go new file mode 100644 index 0000000..b1f1e12 --- /dev/null +++ b/api-gateway/plugins/src/git.pbiernat.dev/traefik/plugin-requestid/uuid.go @@ -0,0 +1,58 @@ +// source: https://github.com/trinnylondon/traefik-add-trace-id/blob/master/rand-utils.go +package plugin_requestid + +import ( + "crypto/rand" + "encoding/hex" + "io" +) + +var rander = rand.Reader // random function +type UUID [16]byte + +func must(uuid UUID, err error) UUID { + if err != nil { + panic(err) + } + return uuid +} + +func newUUID() UUID { + return must(newRandom()) +} + +func newRandom() (UUID, error) { + return newRandomFromReader(rander) +} + +// newRandomFromReader returns a UUID based on bytes read from a given io.Reader. +func newRandomFromReader(r io.Reader) (UUID, error) { + var uuid UUID + _, err := io.ReadFull(r, uuid[:]) + if err != nil { + return UUID{}, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst, uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} diff --git a/data/etc/nginx/apigw.conf b/data/etc/nginx/apigw.conf deleted file mode 100644 index 8287dd4..0000000 --- a/data/etc/nginx/apigw.conf +++ /dev/null @@ -1,43 +0,0 @@ -include apigw_backends.conf; -include apigw_keys.conf; - -server { - access_log /var/log/nginx/apigw_access.log main; - error_log /var/log/nginx/apigw_error.log warn; - - listen 80; - # listen 443 ssl; - # server_name apigw_svc; # container name from stack config - # server_name api.example.com; - - # TLS config - # ssl_certificate /etc/ssl/certs/apigw.example.com.crt; - # ssl_certificate_key /etc/ssl/private/apigw.example.com.key; - # ssl_session_cache shared:SSL:10m; - # ssl_session_timeout 5m; - # ssl_ciphers HIGH:!aNULL:!MD5; - # ssl_protocols TLSv1.2 TLSv1.3; - - # API definitions, one per file - include apigw_conf.d/*.conf; - - # Error responses - # error_page 404 = @400; # Treat invalid paths as bad requests - proxy_intercept_errors on; # Do not send backend errors to client - include apigw_json_errors.conf; # API client-friendly JSON errors - default_type application/json; # If no content-type, assume JSON - - # API key validation - location = /_validate_apikey { - internal; - - if ($http_apikey = "") { - return 401; # Unauthorized - } - if ($apigw_client_name = "") { - return 403; # Forbidden - } - - return 204; # OK (no content) - } -} diff --git a/data/etc/nginx/apigw_backends.conf b/data/etc/nginx/apigw_backends.conf deleted file mode 100644 index 07a6bd4..0000000 --- a/data/etc/nginx/apigw_backends.conf +++ /dev/null @@ -1,9 +0,0 @@ -upstream identity_api { - # zone inventory_service 64k; - server identity_svc:8080; # container name from stack config -} - -upstream basket_api { - # zone pricing_service 64k; - server basket_svc:8080; # container name from stack config -} \ No newline at end of file diff --git a/data/etc/nginx/apigw_conf.d/basket_api.conf b/data/etc/nginx/apigw_conf.d/basket_api.conf deleted file mode 100644 index 16ce9b1..0000000 --- a/data/etc/nginx/apigw_conf.d/basket_api.conf +++ /dev/null @@ -1,14 +0,0 @@ -# Basket API -# -location /api/basket/ { - access_log /var/log/nginx/basket_access.log main; - error_log /var/log/nginx/basket_error.log warn; - - auth_request /_validate_apikey; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header User-Agent "api-gw"; # TMP - FIXME - - proxy_pass http://basket_api/; -} diff --git a/data/etc/nginx/apigw_conf.d/identity_api.conf b/data/etc/nginx/apigw_conf.d/identity_api.conf deleted file mode 100644 index a8b550d..0000000 --- a/data/etc/nginx/apigw_conf.d/identity_api.conf +++ /dev/null @@ -1,32 +0,0 @@ -# Identity API -# -location /api/identity/ { - access_log /var/log/nginx/identity_access.log main; - error_log /var/log/nginx/identity_error.log warn; - - auth_request /_validate_apikey; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header User-Agent "egommerce-api-gateway/0.1"; # TMP - FIXME - - proxy_pass http://identity_api/; -} - -# # URI routing -# # -# location = /api/warehouse/inventory { # Complete inventory -# proxy_pass http://warehouse_inventory; -# } - -# location ~ ^/api/warehouse/inventory/shelf/[^/]+$ { # Shelf inventory -# proxy_pass http://warehouse_inventory; -# } - -# location ~ ^/api/warehouse/inventory/shelf/[^/]+/box/[^/]+$ { # Box on shelf -# proxy_pass http://warehouse_inventory; -# } - -# location ~ ^/api/warehouse/pricing/[^/]+$ { # Price for specific item -# proxy_pass http://warehouse_pricing; -# } diff --git a/data/etc/nginx/apigw_json_errors.conf b/data/etc/nginx/apigw_json_errors.conf deleted file mode 100644 index 9112248..0000000 --- a/data/etc/nginx/apigw_json_errors.conf +++ /dev/null @@ -1,11 +0,0 @@ -error_page 400 = @400; -location @400 { return 400 '{"status":400,"message":"Bad request"}\n'; } - -error_page 401 = @401; -location @401 { return 401 '{"status":401,"message":"Unauthorized"}\n'; } - -error_page 403 = @403; -location @403 { return 403 '{"status":403,"message":"Forbidden"}\n'; } - -error_page 404 = @404; -location @404 { return 404 '{"status":404,"message":"Resource not found"}\n'; } diff --git a/data/etc/nginx/apigw_keys.conf b/data/etc/nginx/apigw_keys.conf deleted file mode 100644 index 61c2b8a..0000000 --- a/data/etc/nginx/apigw_keys.conf +++ /dev/null @@ -1,9 +0,0 @@ -map $http_apikey $apigw_client_name { - default ""; - - "R7HVf14WE5m4d5L3uv2sZU8Y" "identity_api"; - "fd7uAN3/GKIfvFrOdfEAoo1y" "basket_api"; -} - -# $ openssl rand -base64 18 -# 7B5zIqmRGXmrJTFmKa99vcit \ No newline at end of file diff --git a/data/etc/nginx/nginx.conf b/data/etc/nginx/nginx.conf deleted file mode 100644 index bd5575e..0000000 --- a/data/etc/nginx/nginx.conf +++ /dev/null @@ -1,32 +0,0 @@ - -user nginx; -worker_processes auto; - -error_log /var/log/nginx/error.log notice; -pid /var/run/nginx.pid; - - -events { - worker_connections 1024; -} - - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - sendfile on; - #tcp_nopush on; - - keepalive_timeout 65; - - #gzip on; - - include /etc/nginx/apigw.conf; -} diff --git a/deploy/image-build.sh b/deploy/image-build.sh index d37a934..0dbf09e 100755 --- a/deploy/image-build.sh +++ b/deploy/image-build.sh @@ -1,14 +1,14 @@ #!/bin/sh # RUN IN REPO ROOT DIR !! -export IMAGE_NAME="git.pbiernat.dev/egommerce/apigw-svc" +export IMAGE_NAME="git.pbiernat.dev/egommerce/api-gateway" TARGET=${1:-latest} echo "Building: $IMAGE_NAME:$TARGET" if [ $TARGET = "dev" ] then - docker build --rm --no-cache -t "$IMAGE_NAME:dev" . >/dev/null 2>&1 + docker build --rm --no-cache -t "$IMAGE_NAME:dev" . # >/dev/null 2>&1 else docker build --rm --cache-from "$IMAGE_NAME:$TARGET" -t "$IMAGE_NAME:$TARGET" . >/dev/null 2>&1 fi diff --git a/deploy/image-push.sh b/deploy/image-push.sh index 0450bc5..7965a91 100755 --- a/deploy/image-push.sh +++ b/deploy/image-push.sh @@ -1,7 +1,7 @@ #!/bin/sh # RUN IN REPO ROOT DIR !! -export IMAGE_NAME="git.pbiernat.dev/egommerce/apigw-svc" +export IMAGE_NAME="git.pbiernat.dev/egommerce/api-gateway" echo $DOCKER_PASSWORD | docker login git.pbiernat.dev -u $DOCKER_USERNAME --password-stdin docker push "$IMAGE_NAME:latest"