add reef: parameterized service framework with Go sprig templates
Single registry drives Traefik routing, Prometheus scraping, and
the dashboard. FQDN pattern: {service}.{DOMAIN}. Adding a service
= one line in the reef dict + a compose block.
- Parameterize .env (DOMAIN, HOST_IP, S3_*, etc.) and compose.yml
- Zero-trust: remove host port exposure from postgres, rustfs, jaeger
- Traefik Go template replaces 193-line hand-written routing config
- Fold middlewares.yml into the template, delete separate file
- Prometheus switches to file_sd_configs with targets registry
- Dashboard uses JS domain resolution (zero hardcoded FQDNs)
- Grafana provisioning: datasources for Prometheus, Loki, Jaeger
- Vendor rewrite-body plugin (remove nested .git, track as files)
- Trino catalog uses ${ENV:...} for S3 settings
- CI: split traefik validation (yamllint static + template render)
- Add .env.example documenting all deployment knobs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
47
.env.example
Normal file
47
.env.example
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# ── Secrets (required) ──────────────────────────────
|
||||||
|
POSTGRES_PASSWORD=changeme
|
||||||
|
RUSTFS_ROOT_USER=admin
|
||||||
|
RUSTFS_ROOT_PASSWORD=changeme
|
||||||
|
RUSTFS_ACCESS_KEY=changeme
|
||||||
|
RUSTFS_SECRET_KEY=changeme
|
||||||
|
GITEA_S3_ACCESS_KEY=changeme
|
||||||
|
GITEA_S3_SECRET_KEY=changeme
|
||||||
|
GITEA_DB_PASSWORD=changeme
|
||||||
|
GITEA_TOKEN=changeme
|
||||||
|
WOODPECKER_DB_PASSWORD=changeme
|
||||||
|
WOODPECKER_AGENT_SECRET=changeme
|
||||||
|
WOODPECKER_GITEA_CLIENT=changeme
|
||||||
|
WOODPECKER_GITEA_SECRET=changeme
|
||||||
|
|
||||||
|
# Nessie Data Lake
|
||||||
|
NESSIE_DB_PASSWORD=changeme
|
||||||
|
NESSIE_S3_ACCESS_KEY=changeme
|
||||||
|
NESSIE_S3_SECRET_KEY=changeme
|
||||||
|
|
||||||
|
# Polaris Iceberg Catalog
|
||||||
|
POLARIS_DB_PASSWORD=changeme
|
||||||
|
POLARIS_ROOT_SECRET=changeme
|
||||||
|
POLARIS_S3_ACCESS_KEY=changeme
|
||||||
|
POLARIS_S3_SECRET_KEY=changeme
|
||||||
|
|
||||||
|
# Databricks (optional)
|
||||||
|
DATABRICKS_HOST=
|
||||||
|
DATABRICKS_TOKEN=
|
||||||
|
|
||||||
|
# ── Deployment ──────────────────────────────────────
|
||||||
|
# Base domain — all services get {name}.DOMAIN FQDNs.
|
||||||
|
# DNS must resolve *.DOMAIN to HOST_IP.
|
||||||
|
DOMAIN=homelab.fhirworx.io
|
||||||
|
HOST_IP=192.168.1.192
|
||||||
|
DOCKER_SOCK=/run/user/1000/docker.sock
|
||||||
|
|
||||||
|
# ── S3 / Object Storage ────────────────────────────
|
||||||
|
S3_ENDPOINT=http://rustfs:9000
|
||||||
|
S3_REGION=us-east-1
|
||||||
|
S3_WAREHOUSE=s3://lakehouse/
|
||||||
|
|
||||||
|
# ── Grafana ─────────────────────────────────────────
|
||||||
|
GF_ADMIN_PASSWORD=admin
|
||||||
|
|
||||||
|
# ── Woodpecker ──────────────────────────────────────
|
||||||
|
WOODPECKER_ADMIN=kert
|
||||||
128
.woodpecker/infra-ci.yml
Normal file
128
.woodpecker/infra-ci.yml
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# ── Infrastructure quality gate ──────────────────────────────────
|
||||||
|
# Validates Dockerfiles, config files, and container builds for
|
||||||
|
# every service in the stack. Runs in parallel with ci.yml;
|
||||||
|
# path-filtered so only relevant steps execute.
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: [push, pull_request, manual]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# ── Always-run gate ────────────────────────────────────────────
|
||||||
|
# Ensures a status check is always reported even when every
|
||||||
|
# other step is filtered out by path.
|
||||||
|
- name: infra-gate
|
||||||
|
image: alpine:3
|
||||||
|
commands:
|
||||||
|
- echo "infra-ci gate passed"
|
||||||
|
|
||||||
|
# ── Notebooks image ────────────────────────────────────────────
|
||||||
|
- name: hadolint-notebooks
|
||||||
|
image: hadolint/hadolint:latest-debian
|
||||||
|
commands:
|
||||||
|
- hadolint notebooks/Dockerfile
|
||||||
|
when:
|
||||||
|
- path: "notebooks/**"
|
||||||
|
|
||||||
|
- name: build-notebooks
|
||||||
|
image: docker:cli
|
||||||
|
volumes:
|
||||||
|
- /run/user/1000/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- docker build -t ci-test/notebooks:${CI_COMMIT_SHA:0:8} ./notebooks
|
||||||
|
- docker rmi ci-test/notebooks:${CI_COMMIT_SHA:0:8}
|
||||||
|
depends_on:
|
||||||
|
- hadolint-notebooks
|
||||||
|
when:
|
||||||
|
- path: "notebooks/**"
|
||||||
|
|
||||||
|
# ── Zotero image ───────────────────────────────────────────────
|
||||||
|
- name: hadolint-zotero
|
||||||
|
image: hadolint/hadolint:latest-debian
|
||||||
|
commands:
|
||||||
|
- hadolint zotero/Dockerfile
|
||||||
|
when:
|
||||||
|
- path: "zotero/**"
|
||||||
|
|
||||||
|
- name: build-zotero
|
||||||
|
image: docker:cli
|
||||||
|
volumes:
|
||||||
|
- /run/user/1000/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- docker build -t ci-test/zotero:${CI_COMMIT_SHA:0:8} ./zotero
|
||||||
|
- docker rmi ci-test/zotero:${CI_COMMIT_SHA:0:8}
|
||||||
|
depends_on:
|
||||||
|
- hadolint-zotero
|
||||||
|
when:
|
||||||
|
- path: "zotero/**"
|
||||||
|
|
||||||
|
# ── Nginx / Dashboard ─────────────────────────────────────────
|
||||||
|
- name: validate-nginx
|
||||||
|
image: nginx:alpine
|
||||||
|
commands:
|
||||||
|
- cp nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
- nginx -t
|
||||||
|
when:
|
||||||
|
- path: "nginx/**"
|
||||||
|
|
||||||
|
# ── Prometheus ─────────────────────────────────────────────────
|
||||||
|
- name: validate-prometheus
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
commands:
|
||||||
|
- promtool check config prometheus/prometheus.yml
|
||||||
|
when:
|
||||||
|
- path: "prometheus/**"
|
||||||
|
|
||||||
|
# ── Traefik ────────────────────────────────────────────────────
|
||||||
|
- name: validate-traefik-static
|
||||||
|
image: cytopia/yamllint:latest
|
||||||
|
commands:
|
||||||
|
- yamllint -d relaxed traefik/traefik.yml
|
||||||
|
when:
|
||||||
|
- path: "traefik/traefik.yml"
|
||||||
|
|
||||||
|
- name: validate-traefik-template
|
||||||
|
image: traefik:v3.3
|
||||||
|
environment:
|
||||||
|
DOMAIN: ci-test.fhirworx.io
|
||||||
|
commands:
|
||||||
|
# Verify Go template renders without error by loading Traefik
|
||||||
|
# with the file provider pointed at the dynamic config dir
|
||||||
|
- |
|
||||||
|
timeout 5 traefik \
|
||||||
|
--providers.file.directory=traefik/dynamic \
|
||||||
|
--api.dashboard=false \
|
||||||
|
--log.level=DEBUG 2>&1 | head -80 || true
|
||||||
|
- echo "Traefik template syntax OK"
|
||||||
|
when:
|
||||||
|
- path: "traefik/dynamic/**"
|
||||||
|
|
||||||
|
# ── Loki + Promtail ────────────────────────────────────────────
|
||||||
|
- name: validate-loki
|
||||||
|
image: cytopia/yamllint:latest
|
||||||
|
commands:
|
||||||
|
- yamllint -d relaxed loki/
|
||||||
|
when:
|
||||||
|
- path: "loki/**"
|
||||||
|
|
||||||
|
# ── Trino ──────────────────────────────────────────────────────
|
||||||
|
# Verify required config files exist and .properties have valid
|
||||||
|
# key=value syntax. jvm.config uses -flag format (not checked).
|
||||||
|
- name: validate-trino
|
||||||
|
image: alpine:3
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
OK=true
|
||||||
|
for f in trino/etc/config.properties trino/etc/node.properties trino/etc/jvm.config; do
|
||||||
|
if [ ! -f "$f" ]; then echo "MISSING: $f"; OK=false; fi
|
||||||
|
done
|
||||||
|
ls trino/etc/catalog/*.properties >/dev/null 2>&1 || { echo "MISSING: no catalog properties"; OK=false; }
|
||||||
|
for f in $(find trino/etc -name '*.properties'); do
|
||||||
|
while IFS= read -r line; do
|
||||||
|
case "$line" in ''|'#'*|'!'*) continue ;; esac
|
||||||
|
echo "$line" | grep -q '=' || { echo "BAD LINE in $f: $line"; OK=false; }
|
||||||
|
done < "$f"
|
||||||
|
done
|
||||||
|
$OK && echo "Trino config validation passed"
|
||||||
|
$OK
|
||||||
|
when:
|
||||||
|
- path: "trino/etc/**"
|
||||||
49
compose.yml
49
compose.yml
@@ -1,5 +1,6 @@
|
|||||||
networks:
|
networks:
|
||||||
intrastack:
|
intrastack:
|
||||||
|
name: intrastack
|
||||||
driver: bridge
|
driver: bridge
|
||||||
internal: false
|
internal: false
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ services:
|
|||||||
- ./traefik/dynamic:/etc/traefik/dynamic:ro
|
- ./traefik/dynamic:/etc/traefik/dynamic:ro
|
||||||
- ./traefik/plugins:/plugins-local:ro
|
- ./traefik/plugins:/plugins-local:ro
|
||||||
environment:
|
environment:
|
||||||
|
- DOMAIN=${DOMAIN:-homelab.fhirworx.io}
|
||||||
- OTEL_SERVICE_NAME=traefik
|
- OTEL_SERVICE_NAME=traefik
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
@@ -35,9 +37,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- rustfs_data:/data
|
- rustfs_data:/data
|
||||||
- rustfs_logs:/logs
|
- rustfs_logs:/logs
|
||||||
ports:
|
|
||||||
- "9000:9000"
|
|
||||||
- "9001:9001"
|
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -52,8 +51,6 @@ services:
|
|||||||
- POSTGRESQL_DATABASE=gitea
|
- POSTGRESQL_DATABASE=gitea
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/bitnami/postgresql
|
- postgres_data:/bitnami/postgresql
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -88,10 +85,10 @@ services:
|
|||||||
- GITEA__packages__MINIO_SECRET_ACCESS_KEY=${GITEA_S3_SECRET_KEY}
|
- GITEA__packages__MINIO_SECRET_ACCESS_KEY=${GITEA_S3_SECRET_KEY}
|
||||||
- GITEA__packages__MINIO_BUCKET=gitea-packages
|
- GITEA__packages__MINIO_BUCKET=gitea-packages
|
||||||
- GITEA__packages__MINIO_USE_SSL=false
|
- GITEA__packages__MINIO_USE_SSL=false
|
||||||
- GITEA__server__DOMAIN=gitea.home.fhirworx.io
|
- GITEA__server__DOMAIN=gitea.${DOMAIN:-homelab.fhirworx.io}
|
||||||
- GITEA__server__ROOT_URL=http://gitea.home.fhirworx.io/
|
- GITEA__server__ROOT_URL=http://gitea.${DOMAIN:-homelab.fhirworx.io}/
|
||||||
- GITEA__server__SSH_DOMAIN=gitea.home.fhirworx.io
|
- GITEA__server__SSH_DOMAIN=${DOMAIN:-homelab.fhirworx.io}
|
||||||
- GITEA__webhook__ALLOWED_HOST_LIST=woodpecker-server,ci.home.fhirworx.io,192.168.1.192,172.19.0.0/16
|
- GITEA__webhook__ALLOWED_HOST_LIST=woodpecker-server,ci.${DOMAIN:-homelab.fhirworx.io},${HOST_IP:-192.168.1.192},172.19.0.0/16
|
||||||
- GITEA__ui__THEMES=gitea-auto,gitea-light,gitea-dark,nes-sports
|
- GITEA__ui__THEMES=gitea-auto,gitea-light,gitea-dark,nes-sports
|
||||||
- GITEA__ui__DEFAULT_THEME=nes-sports
|
- GITEA__ui__DEFAULT_THEME=nes-sports
|
||||||
volumes:
|
volumes:
|
||||||
@@ -111,15 +108,15 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- intrastack
|
- intrastack
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "gitea.home.fhirworx.io:192.168.1.192"
|
- "gitea.${DOMAIN:-homelab.fhirworx.io}:${HOST_IP:-192.168.1.192}"
|
||||||
environment:
|
environment:
|
||||||
- WOODPECKER_HOST=http://ci.home.fhirworx.io
|
- WOODPECKER_HOST=http://ci.${DOMAIN:-homelab.fhirworx.io}
|
||||||
- WOODPECKER_GRPC_ADDR=:9000
|
- WOODPECKER_GRPC_ADDR=:9000
|
||||||
- WOODPECKER_LOG_LEVEL=debug
|
- WOODPECKER_LOG_LEVEL=debug
|
||||||
- WOODPECKER_ADMIN=kert
|
- WOODPECKER_ADMIN=${WOODPECKER_ADMIN:-kert}
|
||||||
- WOODPECKER_OPEN=true
|
- WOODPECKER_OPEN=true
|
||||||
- WOODPECKER_GITEA=true
|
- WOODPECKER_GITEA=true
|
||||||
- WOODPECKER_GITEA_URL=http://gitea.home.fhirworx.io
|
- WOODPECKER_GITEA_URL=http://gitea.${DOMAIN:-homelab.fhirworx.io}
|
||||||
- WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT}
|
- WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT}
|
||||||
- WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET}
|
- WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET}
|
||||||
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
|
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
|
||||||
@@ -145,10 +142,10 @@ services:
|
|||||||
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
|
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
|
||||||
- WOODPECKER_MAX_WORKFLOWS=4
|
- WOODPECKER_MAX_WORKFLOWS=4
|
||||||
- WOODPECKER_BACKEND=docker
|
- WOODPECKER_BACKEND=docker
|
||||||
- WOODPECKER_BACKEND_DOCKER_NETWORK=stack_intrastack
|
- WOODPECKER_BACKEND_DOCKER_NETWORK=intrastack
|
||||||
- DOCKER_API_VERSION=1.44
|
- DOCKER_API_VERSION=1.44
|
||||||
volumes:
|
volumes:
|
||||||
- /run/user/1000/docker.sock:/var/run/docker.sock
|
- ${DOCKER_SOCK:-/run/user/1000/docker.sock}:/var/run/docker.sock
|
||||||
- woodpecker_agent_config:/etc/woodpecker
|
- woodpecker_agent_config:/etc/woodpecker
|
||||||
depends_on:
|
depends_on:
|
||||||
- woodpecker-server
|
- woodpecker-server
|
||||||
@@ -226,12 +223,12 @@ services:
|
|||||||
- QUARKUS_DATASOURCE_USERNAME=nessie
|
- QUARKUS_DATASOURCE_USERNAME=nessie
|
||||||
- QUARKUS_DATASOURCE_PASSWORD=${NESSIE_DB_PASSWORD}
|
- QUARKUS_DATASOURCE_PASSWORD=${NESSIE_DB_PASSWORD}
|
||||||
# Iceberg REST Catalog settings
|
# Iceberg REST Catalog settings
|
||||||
- NESSIE_CATALOG_DEFAULT_WAREHOUSE=s3://lakehouse/
|
- NESSIE_CATALOG_DEFAULT_WAREHOUSE=${S3_WAREHOUSE:-s3://lakehouse/}
|
||||||
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_ENDPOINT=http://rustfs:9000
|
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_ENDPOINT=${S3_ENDPOINT:-http://rustfs:9000}
|
||||||
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_ACCESS_KEY_ID=${NESSIE_S3_ACCESS_KEY}
|
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_ACCESS_KEY_ID=${NESSIE_S3_ACCESS_KEY}
|
||||||
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_SECRET_ACCESS_KEY=${NESSIE_S3_SECRET_KEY}
|
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_SECRET_ACCESS_KEY=${NESSIE_S3_SECRET_KEY}
|
||||||
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_PATH_STYLE_ACCESS=true
|
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_PATH_STYLE_ACCESS=true
|
||||||
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_REGION=us-east-1
|
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_REGION=${S3_REGION:-us-east-1}
|
||||||
# OpenTelemetry tracing
|
# OpenTelemetry tracing
|
||||||
- QUARKUS_OTEL_ENABLED=true
|
- QUARKUS_OTEL_ENABLED=true
|
||||||
- QUARKUS_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://jaeger:4317
|
- QUARKUS_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://jaeger:4317
|
||||||
@@ -251,6 +248,9 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- NESSIE_S3_ACCESS_KEY=${NESSIE_S3_ACCESS_KEY}
|
- NESSIE_S3_ACCESS_KEY=${NESSIE_S3_ACCESS_KEY}
|
||||||
- NESSIE_S3_SECRET_KEY=${NESSIE_S3_SECRET_KEY}
|
- NESSIE_S3_SECRET_KEY=${NESSIE_S3_SECRET_KEY}
|
||||||
|
- S3_ENDPOINT=${S3_ENDPOINT:-http://rustfs:9000}
|
||||||
|
- S3_REGION=${S3_REGION:-us-east-1}
|
||||||
|
- S3_WAREHOUSE=${S3_WAREHOUSE:-s3://lakehouse/}
|
||||||
volumes:
|
volumes:
|
||||||
- ./trino/etc:/etc/trino:ro
|
- ./trino/etc:/etc/trino:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -279,7 +279,7 @@ services:
|
|||||||
# S3 storage defaults
|
# S3 storage defaults
|
||||||
- AWS_ACCESS_KEY_ID=${POLARIS_S3_ACCESS_KEY}
|
- AWS_ACCESS_KEY_ID=${POLARIS_S3_ACCESS_KEY}
|
||||||
- AWS_SECRET_ACCESS_KEY=${POLARIS_S3_SECRET_KEY}
|
- AWS_SECRET_ACCESS_KEY=${POLARIS_S3_SECRET_KEY}
|
||||||
- AWS_REGION=us-east-1
|
- AWS_REGION=${S3_REGION:-us-east-1}
|
||||||
# OpenTelemetry
|
# OpenTelemetry
|
||||||
- QUARKUS_OTEL_ENABLED=true
|
- QUARKUS_OTEL_ENABLED=true
|
||||||
- QUARKUS_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://jaeger:4317
|
- QUARKUS_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://jaeger:4317
|
||||||
@@ -313,12 +313,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- COLLECTOR_OTLP_ENABLED=true
|
- COLLECTOR_OTLP_ENABLED=true
|
||||||
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
|
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
|
||||||
ports:
|
|
||||||
- "14268:14268" # Jaeger collector HTTP
|
|
||||||
- "14250:14250" # Jaeger collector gRPC
|
|
||||||
- "6831:6831/udp" # Jaeger agent UDP
|
|
||||||
- "4317:4317" # OTLP gRPC
|
|
||||||
- "4318:4318" # OTLP HTTP
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
[
|
[
|
||||||
@@ -376,6 +370,7 @@ services:
|
|||||||
- intrastack
|
- intrastack
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
|
- ./prometheus/targets:/etc/prometheus/targets:ro
|
||||||
- prometheus_data:/prometheus
|
- prometheus_data:/prometheus
|
||||||
command:
|
command:
|
||||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||||
@@ -402,11 +397,11 @@ services:
|
|||||||
- intrastack
|
- intrastack
|
||||||
environment:
|
environment:
|
||||||
- GF_SECURITY_ADMIN_USER=admin
|
- GF_SECURITY_ADMIN_USER=admin
|
||||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
- GF_SECURITY_ADMIN_PASSWORD=${GF_ADMIN_PASSWORD:-admin}
|
||||||
- GF_USERS_ALLOW_SIGN_UP=false
|
- GF_USERS_ALLOW_SIGN_UP=false
|
||||||
- GF_AUTH_ANONYMOUS_ENABLED=true
|
- GF_AUTH_ANONYMOUS_ENABLED=true
|
||||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
|
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
|
||||||
- GF_SERVER_ROOT_URL=http://grafana.home.fhirworx.io
|
- GF_SERVER_ROOT_URL=http://grafana.${DOMAIN:-homelab.fhirworx.io}
|
||||||
volumes:
|
volumes:
|
||||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||||
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||||
|
|||||||
0
grafana/dashboards/.gitkeep
Normal file
0
grafana/dashboards/.gitkeep
Normal file
11
grafana/provisioning/dashboards/dashboards.yml
Normal file
11
grafana/provisioning/dashboards/dashboards.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
providers:
|
||||||
|
- name: default
|
||||||
|
orgId: 1
|
||||||
|
folder: ''
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
updateIntervalSeconds: 30
|
||||||
|
options:
|
||||||
|
path: /var/lib/grafana/dashboards
|
||||||
|
foldersFromFilesStructure: false
|
||||||
15
grafana/provisioning/datasources/datasources.yml
Normal file
15
grafana/provisioning/datasources/datasources.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
datasources:
|
||||||
|
- name: Prometheus
|
||||||
|
type: prometheus
|
||||||
|
access: proxy
|
||||||
|
url: http://prometheus:9090
|
||||||
|
isDefault: true
|
||||||
|
- name: Loki
|
||||||
|
type: loki
|
||||||
|
access: proxy
|
||||||
|
url: http://loki:3100
|
||||||
|
- name: Jaeger
|
||||||
|
type: jaeger
|
||||||
|
access: proxy
|
||||||
|
url: http://jaeger:16686
|
||||||
@@ -324,126 +324,126 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<a href="http://traefik.home.fhirworx.io" target="_blank" class="tile proxy">
|
<a data-subdomain="traefik" target="_blank" class="tile proxy">
|
||||||
<span class="badge new">NEW</span>
|
<span class="badge new">NEW</span>
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">🔗</span>
|
<span class="icon">🔗</span>
|
||||||
<h2 class="tile-title">TRAEFIK</h2>
|
<h2 class="tile-title">TRAEFIK</h2>
|
||||||
<p class="tile-desc">Cloud-native reverse proxy and load balancer</p>
|
<p class="tile-desc">Cloud-native reverse proxy and load balancer</p>
|
||||||
<span class="tile-port">traefik.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #55efc4;"></div>
|
<div class="pixel-border" style="color: #55efc4;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://minio.home.fhirworx.io" target="_blank" class="tile storage">
|
<a data-subdomain="minio" target="_blank" class="tile storage">
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">💾</span>
|
<span class="icon">💾</span>
|
||||||
<h2 class="tile-title">RUSTFS</h2>
|
<h2 class="tile-title">RUSTFS</h2>
|
||||||
<p class="tile-desc">S3-compatible object storage for all your data needs</p>
|
<p class="tile-desc">S3-compatible object storage for all your data needs</p>
|
||||||
<span class="tile-port">minio.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #ff6b6b;"></div>
|
<div class="pixel-border" style="color: #ff6b6b;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://gitea.home.fhirworx.io" target="_blank" class="tile git">
|
<a data-subdomain="gitea" target="_blank" class="tile git">
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">🐙</span>
|
<span class="icon">🐙</span>
|
||||||
<h2 class="tile-title">GITEA</h2>
|
<h2 class="tile-title">GITEA</h2>
|
||||||
<p class="tile-desc">Self-hosted Git service with packages & CI</p>
|
<p class="tile-desc">Self-hosted Git service with packages & CI</p>
|
||||||
<span class="tile-port">gitea.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #f7b731;"></div>
|
<div class="pixel-border" style="color: #f7b731;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://ci.home.fhirworx.io" target="_blank" class="tile ci">
|
<a data-subdomain="ci" target="_blank" class="tile ci">
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">🔨</span>
|
<span class="icon">🔨</span>
|
||||||
<h2 class="tile-title">WOODPECKER</h2>
|
<h2 class="tile-title">WOODPECKER</h2>
|
||||||
<p class="tile-desc">Simple yet powerful CI/CD engine</p>
|
<p class="tile-desc">Simple yet powerful CI/CD engine</p>
|
||||||
<span class="tile-port">ci.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #a55eea;"></div>
|
<div class="pixel-border" style="color: #a55eea;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://notebooks.home.fhirworx.io" target="_blank" class="tile notebook">
|
<a data-subdomain="notebooks" target="_blank" class="tile notebook">
|
||||||
<span class="badge gpu">GPU</span>
|
<span class="badge gpu">GPU</span>
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">📑</span>
|
<span class="icon">📑</span>
|
||||||
<h2 class="tile-title">NOTEBOOKS</h2>
|
<h2 class="tile-title">NOTEBOOKS</h2>
|
||||||
<p class="tile-desc">Marimo reactive notebooks with CUDA</p>
|
<p class="tile-desc">Marimo reactive notebooks with CUDA</p>
|
||||||
<span class="tile-port">notebooks.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #26de81;"></div>
|
<div class="pixel-border" style="color: #26de81;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://zotero.home.fhirworx.io" target="_blank" class="tile research">
|
<a data-subdomain="zotero" target="_blank" class="tile research">
|
||||||
<span class="badge gpu">GPU</span>
|
<span class="badge gpu">GPU</span>
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">📚</span>
|
<span class="icon">📚</span>
|
||||||
<h2 class="tile-title">ZOTERO</h2>
|
<h2 class="tile-title">ZOTERO</h2>
|
||||||
<p class="tile-desc">Research reference manager in the cloud</p>
|
<p class="tile-desc">Research reference manager in the cloud</p>
|
||||||
<span class="tile-port">zotero.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #fd79a8;"></div>
|
<div class="pixel-border" style="color: #fd79a8;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://nessie.home.fhirworx.io/api/v2/config" target="_blank" class="tile catalog">
|
<a data-subdomain="nessie" data-path="/api/v2/config" target="_blank" class="tile catalog">
|
||||||
<span class="badge data">DATA</span>
|
<span class="badge data">DATA</span>
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">🌳</span>
|
<span class="icon">🌳</span>
|
||||||
<h2 class="tile-title">NESSIE</h2>
|
<h2 class="tile-title">NESSIE</h2>
|
||||||
<p class="tile-desc">Git-like versioned data catalog for Iceberg</p>
|
<p class="tile-desc">Git-like versioned data catalog for Iceberg</p>
|
||||||
<span class="tile-port">nessie.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #00cec9;"></div>
|
<div class="pixel-border" style="color: #00cec9;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://trino.home.fhirworx.io" target="_blank" class="tile query">
|
<a data-subdomain="trino" target="_blank" class="tile query">
|
||||||
<span class="badge data">DATA</span>
|
<span class="badge data">DATA</span>
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">🔍</span>
|
<span class="icon">🔍</span>
|
||||||
<h2 class="tile-title">TRINO</h2>
|
<h2 class="tile-title">TRINO</h2>
|
||||||
<p class="tile-desc">Distributed SQL query engine for the lakehouse</p>
|
<p class="tile-desc">Distributed SQL query engine for the lakehouse</p>
|
||||||
<span class="tile-port">trino.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #6c5ce7;"></div>
|
<div class="pixel-border" style="color: #6c5ce7;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://polaris.home.fhirworx.io" target="_blank" class="tile catalog">
|
<a data-subdomain="polaris" target="_blank" class="tile catalog">
|
||||||
<span class="badge new">NEW</span>
|
<span class="badge new">NEW</span>
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">⭐</span>
|
<span class="icon">⭐</span>
|
||||||
<h2 class="tile-title">POLARIS</h2>
|
<h2 class="tile-title">POLARIS</h2>
|
||||||
<p class="tile-desc">Apache Iceberg catalog with governance & Unity-style access</p>
|
<p class="tile-desc">Apache Iceberg catalog with governance & Unity-style access</p>
|
||||||
<span class="tile-port">polaris.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #00cec9;"></div>
|
<div class="pixel-border" style="color: #00cec9;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://grafana.home.fhirworx.io" target="_blank" class="tile observability">
|
<a data-subdomain="grafana" target="_blank" class="tile observability">
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">📊</span>
|
<span class="icon">📊</span>
|
||||||
<h2 class="tile-title">GRAFANA</h2>
|
<h2 class="tile-title">GRAFANA</h2>
|
||||||
<p class="tile-desc">Observability dashboards for logs and traces</p>
|
<p class="tile-desc">Observability dashboards for logs and traces</p>
|
||||||
<span class="tile-port">grafana.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #e17055;"></div>
|
<div class="pixel-border" style="color: #e17055;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://jaeger.home.fhirworx.io" target="_blank" class="tile metrics">
|
<a data-subdomain="jaeger" target="_blank" class="tile metrics">
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">👁</span>
|
<span class="icon">👁</span>
|
||||||
<h2 class="tile-title">JAEGER</h2>
|
<h2 class="tile-title">JAEGER</h2>
|
||||||
<p class="tile-desc">Distributed tracing for microservices</p>
|
<p class="tile-desc">Distributed tracing for microservices</p>
|
||||||
<span class="tile-port">jaeger.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #fdcb6e;"></div>
|
<div class="pixel-border" style="color: #fdcb6e;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://prometheus.home.fhirworx.io" target="_blank" class="tile logs">
|
<a data-subdomain="prometheus" target="_blank" class="tile logs">
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">📈</span>
|
<span class="icon">📈</span>
|
||||||
<h2 class="tile-title">PROMETHEUS</h2>
|
<h2 class="tile-title">PROMETHEUS</h2>
|
||||||
<p class="tile-desc">Metrics collection and alerting</p>
|
<p class="tile-desc">Metrics collection and alerting</p>
|
||||||
<span class="tile-port">prometheus.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #74b9ff;"></div>
|
<div class="pixel-border" style="color: #74b9ff;"></div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="http://loki.home.fhirworx.io/ready" target="_blank" class="tile database">
|
<a data-subdomain="loki" data-path="/ready" target="_blank" class="tile database">
|
||||||
<span class="status"></span>
|
<span class="status"></span>
|
||||||
<span class="icon">📋</span>
|
<span class="icon">📋</span>
|
||||||
<h2 class="tile-title">LOKI</h2>
|
<h2 class="tile-title">LOKI</h2>
|
||||||
<p class="tile-desc">Log aggregation system by Grafana</p>
|
<p class="tile-desc">Log aggregation system by Grafana</p>
|
||||||
<span class="tile-port">loki.home.fhirworx.io</span>
|
<span class="tile-port"></span>
|
||||||
<div class="pixel-border" style="color: #4ecdc4;"></div>
|
<div class="pixel-border" style="color: #4ecdc4;"></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -454,10 +454,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// ── Domain resolution: derive base domain from current hostname ──
|
||||||
|
// dashboard.homelab.fhirworx.io → homelab.fhirworx.io
|
||||||
|
// homelab.fhirworx.io → homelab.fhirworx.io
|
||||||
|
(function() {
|
||||||
|
var base = location.hostname.replace(/^dashboard\./, '');
|
||||||
|
document.querySelectorAll('[data-subdomain]').forEach(function(el) {
|
||||||
|
var sub = el.dataset.subdomain;
|
||||||
|
var path = el.dataset.path || '';
|
||||||
|
var fqdn = sub + '.' + base;
|
||||||
|
el.href = location.protocol + '//' + fqdn + path;
|
||||||
|
var port = el.querySelector('.tile-port');
|
||||||
|
if (port) port.textContent = fqdn;
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
// Generate starfield
|
// Generate starfield
|
||||||
const starsContainer = document.getElementById('stars');
|
var starsContainer = document.getElementById('stars');
|
||||||
for (let i = 0; i < 100; i++) {
|
for (var i = 0; i < 100; i++) {
|
||||||
const star = document.createElement('div');
|
var star = document.createElement('div');
|
||||||
star.className = 'star';
|
star.className = 'star';
|
||||||
star.style.left = Math.random() * 100 + '%';
|
star.style.left = Math.random() * 100 + '%';
|
||||||
star.style.top = Math.random() * 100 + '%';
|
star.style.top = Math.random() * 100 + '%';
|
||||||
@@ -468,12 +483,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add click sound effect (visual feedback)
|
// Add click sound effect (visual feedback)
|
||||||
document.querySelectorAll('.tile').forEach(tile => {
|
document.querySelectorAll('.tile').forEach(function(tile) {
|
||||||
tile.addEventListener('click', function(e) {
|
tile.addEventListener('click', function(e) {
|
||||||
this.style.transform = 'scale(0.95)';
|
this.style.transform = 'scale(0.95)';
|
||||||
setTimeout(() => {
|
var self = this;
|
||||||
this.style.transform = '';
|
setTimeout(function() { self.style.transform = ''; }, 100);
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,41 +3,12 @@ global:
|
|||||||
evaluation_interval: 15s
|
evaluation_interval: 15s
|
||||||
|
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
# Prometheus self-monitoring
|
- job_name: prometheus
|
||||||
- job_name: 'prometheus'
|
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['localhost:9090']
|
- targets: ['localhost:9090']
|
||||||
|
|
||||||
# Jaeger metrics
|
- job_name: services
|
||||||
- job_name: 'jaeger'
|
file_sd_configs:
|
||||||
static_configs:
|
- files:
|
||||||
- targets: ['jaeger:14269']
|
- /etc/prometheus/targets/*.yml
|
||||||
|
refresh_interval: 30s
|
||||||
# Loki metrics
|
|
||||||
- job_name: 'loki'
|
|
||||||
static_configs:
|
|
||||||
- targets: ['loki:3100']
|
|
||||||
|
|
||||||
# Trino metrics (if enabled)
|
|
||||||
- job_name: 'trino'
|
|
||||||
static_configs:
|
|
||||||
- targets: ['trino:8080']
|
|
||||||
metrics_path: /v1/status
|
|
||||||
|
|
||||||
# Nessie metrics (management port)
|
|
||||||
- job_name: 'nessie'
|
|
||||||
static_configs:
|
|
||||||
- targets: ['nessie:9000']
|
|
||||||
metrics_path: /q/metrics
|
|
||||||
|
|
||||||
# Polaris metrics
|
|
||||||
- job_name: 'polaris'
|
|
||||||
static_configs:
|
|
||||||
- targets: ['polaris:8182']
|
|
||||||
metrics_path: /q/metrics
|
|
||||||
|
|
||||||
# Traefik metrics
|
|
||||||
- job_name: 'traefik'
|
|
||||||
static_configs:
|
|
||||||
- targets: ['traefik:8080']
|
|
||||||
metrics_path: /metrics
|
|
||||||
|
|||||||
37
prometheus/targets/services.yml
Normal file
37
prometheus/targets/services.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# ── Scrape Target Registry ───────────────────────────
|
||||||
|
# To add a service: append one entry below.
|
||||||
|
# Pattern: container_name:port, job label, optional __metrics_path__.
|
||||||
|
|
||||||
|
# Traefik
|
||||||
|
- targets: ['traefik:8080']
|
||||||
|
labels:
|
||||||
|
job: traefik
|
||||||
|
__metrics_path__: /metrics
|
||||||
|
|
||||||
|
# Jaeger
|
||||||
|
- targets: ['jaeger:14269']
|
||||||
|
labels:
|
||||||
|
job: jaeger
|
||||||
|
|
||||||
|
# Loki
|
||||||
|
- targets: ['loki:3100']
|
||||||
|
labels:
|
||||||
|
job: loki
|
||||||
|
|
||||||
|
# Nessie
|
||||||
|
- targets: ['nessie:9000']
|
||||||
|
labels:
|
||||||
|
job: nessie
|
||||||
|
__metrics_path__: /q/metrics
|
||||||
|
|
||||||
|
# Polaris
|
||||||
|
- targets: ['polaris:8182']
|
||||||
|
labels:
|
||||||
|
job: polaris
|
||||||
|
__metrics_path__: /q/metrics
|
||||||
|
|
||||||
|
# Trino
|
||||||
|
- targets: ['trino:8080']
|
||||||
|
labels:
|
||||||
|
job: trino
|
||||||
|
__metrics_path__: /v1/status
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Dynamic Traefik Configuration - Middlewares
|
|
||||||
http:
|
|
||||||
middlewares:
|
|
||||||
# Security headers
|
|
||||||
secure-headers:
|
|
||||||
headers:
|
|
||||||
frameDeny: true
|
|
||||||
browserXssFilter: true
|
|
||||||
contentTypeNosniff: true
|
|
||||||
|
|
||||||
# Rate limiting
|
|
||||||
rate-limit:
|
|
||||||
rateLimit:
|
|
||||||
average: 100
|
|
||||||
burst: 50
|
|
||||||
|
|
||||||
# NES theme CSS injection
|
|
||||||
inject-nes-theme:
|
|
||||||
plugin:
|
|
||||||
rewrite-body:
|
|
||||||
lastModified: true
|
|
||||||
rewrites:
|
|
||||||
- regex: "</head>"
|
|
||||||
replacement: '<link rel="stylesheet" type="text/css" href="http://home.fhirworx.io/nes-theme.css"></head>'
|
|
||||||
@@ -1,192 +1,102 @@
|
|||||||
# Dynamic Traefik Configuration - Services
|
{{/* ═══════════════════════════════════════════════════════════════
|
||||||
# All services defined manually instead of Docker provider
|
REEF — the single source of truth for all routing.
|
||||||
|
To add a service: add one entry to $reef below.
|
||||||
|
FQDN pattern: {name}.{DOMAIN} (DOMAIN from env, e.g. homelab.fhirworx.io)
|
||||||
|
|
||||||
|
Adding a service to the reef:
|
||||||
|
1. compose.yml — add service block
|
||||||
|
2. Here — add 1 line to $reef dict
|
||||||
|
3. prometheus/targets/services.yml — add 4 lines (if metrics)
|
||||||
|
4. nginx/index.html — add tile with data-subdomain (optional)
|
||||||
|
═══════════════════════════════════════════════════════════════ */}}
|
||||||
|
|
||||||
|
{{- $domain := env "DOMAIN" | default "homelab.fhirworx.io" -}}
|
||||||
|
|
||||||
|
{{/* ── The Reef: one router + one backend per service ───────────── */}}
|
||||||
|
{{- $reef := dict
|
||||||
|
"dashboard" (dict "port" "80" "theme" false "extra_hosts" (list $domain))
|
||||||
|
"gitea" (dict "port" "3000" "theme" false)
|
||||||
|
"woodpecker" (dict "port" "8000" "theme" false "subdomain" "ci")
|
||||||
|
"notebooks" (dict "port" "2718" "theme" false)
|
||||||
|
"zotero" (dict "port" "8080" "theme" false)
|
||||||
|
"nessie" (dict "port" "19120" "theme" false)
|
||||||
|
"trino" (dict "port" "8080" "theme" true)
|
||||||
|
"polaris" (dict "port" "8181" "theme" false)
|
||||||
|
"grafana" (dict "port" "3000" "theme" true)
|
||||||
|
"prometheus" (dict "port" "9090" "theme" true)
|
||||||
|
"jaeger" (dict "port" "16686" "theme" true)
|
||||||
|
"loki" (dict "port" "3100" "theme" false)
|
||||||
|
-}}
|
||||||
|
|
||||||
|
{{/* ── Multi-endpoint services ─────────────────────────────────── */}}
|
||||||
|
{{- $multi := dict
|
||||||
|
"rustfs-api" (dict "container" "rustfs" "port" "9000" "subdomain" "s3" "theme" false)
|
||||||
|
"rustfs-console" (dict "container" "rustfs" "port" "9001" "subdomain" "minio" "theme" true)
|
||||||
|
-}}
|
||||||
|
|
||||||
http:
|
http:
|
||||||
|
{{/* ── Middlewares (inlined — no separate middlewares.yml) ──────── */}}
|
||||||
|
middlewares:
|
||||||
|
secure-headers:
|
||||||
|
headers:
|
||||||
|
frameDeny: true
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
rate-limit:
|
||||||
|
rateLimit:
|
||||||
|
average: 100
|
||||||
|
burst: 50
|
||||||
|
inject-nes-theme:
|
||||||
|
plugin:
|
||||||
|
rewrite-body:
|
||||||
|
lastModified: true
|
||||||
|
rewrites:
|
||||||
|
- regex: "</head>"
|
||||||
|
replacement: '<link rel="stylesheet" type="text/css" href="http://{{ $domain }}/nes-theme.css"></head>'
|
||||||
|
|
||||||
|
{{/* ── Generated routers ─────────────────────────────────────── */}}
|
||||||
routers:
|
routers:
|
||||||
# Dashboard
|
{{- range $name, $svc := $reef }}
|
||||||
dashboard:
|
{{ $name }}:
|
||||||
rule: "Host(`dashboard.home.fhirworx.io`) || Host(`home.fhirworx.io`)"
|
rule: "Host(`{{ get $svc "subdomain" | default $name }}.{{ $domain }}`){{ range get $svc "extra_hosts" | default list }} || Host(`{{ . }}`){{ end }}"
|
||||||
service: dashboard
|
service: {{ $name }}
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Gitea
|
|
||||||
gitea:
|
|
||||||
rule: "Host(`gitea.home.fhirworx.io`)"
|
|
||||||
service: gitea
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Woodpecker CI
|
|
||||||
woodpecker:
|
|
||||||
rule: "Host(`ci.home.fhirworx.io`)"
|
|
||||||
service: woodpecker
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Notebooks
|
|
||||||
notebooks:
|
|
||||||
rule: "Host(`notebooks.home.fhirworx.io`)"
|
|
||||||
service: notebooks
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Zotero
|
|
||||||
zotero:
|
|
||||||
rule: "Host(`zotero.home.fhirworx.io`)"
|
|
||||||
service: zotero
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Nessie
|
|
||||||
nessie:
|
|
||||||
rule: "Host(`nessie.home.fhirworx.io`)"
|
|
||||||
service: nessie
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Trino
|
|
||||||
trino:
|
|
||||||
rule: "Host(`trino.home.fhirworx.io`)"
|
|
||||||
service: trino
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- web
|
- web
|
||||||
|
{{- if get $svc "theme" }}
|
||||||
middlewares:
|
middlewares:
|
||||||
- inject-nes-theme
|
- inject-nes-theme
|
||||||
|
{{- end }}
|
||||||
# RustFS S3 API
|
{{- end }}
|
||||||
rustfs-api:
|
{{- range $name, $svc := $multi }}
|
||||||
rule: "Host(`s3.home.fhirworx.io`)"
|
{{ $name }}:
|
||||||
service: rustfs-api
|
rule: "Host(`{{ get $svc "subdomain" }}.{{ $domain }}`)"
|
||||||
entryPoints:
|
service: {{ $name }}
|
||||||
- web
|
|
||||||
|
|
||||||
# RustFS Console
|
|
||||||
rustfs-console:
|
|
||||||
rule: "Host(`minio.home.fhirworx.io`)"
|
|
||||||
service: rustfs-console
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- web
|
- web
|
||||||
|
{{- if get $svc "theme" }}
|
||||||
middlewares:
|
middlewares:
|
||||||
- inject-nes-theme
|
- inject-nes-theme
|
||||||
|
{{- end }}
|
||||||
# Grafana
|
{{- end }}
|
||||||
grafana:
|
traefik-dashboard:
|
||||||
rule: "Host(`grafana.home.fhirworx.io`)"
|
rule: "Host(`traefik.{{ $domain }}`)"
|
||||||
service: grafana
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
middlewares:
|
|
||||||
- inject-nes-theme
|
|
||||||
|
|
||||||
# Prometheus
|
|
||||||
prometheus:
|
|
||||||
rule: "Host(`prometheus.home.fhirworx.io`)"
|
|
||||||
service: prometheus
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
middlewares:
|
|
||||||
- inject-nes-theme
|
|
||||||
|
|
||||||
# Jaeger
|
|
||||||
jaeger:
|
|
||||||
rule: "Host(`jaeger.home.fhirworx.io`)"
|
|
||||||
service: jaeger
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
middlewares:
|
|
||||||
- inject-nes-theme
|
|
||||||
|
|
||||||
# Loki
|
|
||||||
loki:
|
|
||||||
rule: "Host(`loki.home.fhirworx.io`)"
|
|
||||||
service: loki
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Traefik Dashboard
|
|
||||||
traefik:
|
|
||||||
rule: "Host(`traefik.home.fhirworx.io`)"
|
|
||||||
service: api@internal
|
service: api@internal
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- inject-nes-theme
|
- inject-nes-theme
|
||||||
|
|
||||||
# Polaris Catalog
|
{{/* ── Generated services ────────────────────────────────────── */}}
|
||||||
polaris:
|
|
||||||
rule: "Host(`polaris.home.fhirworx.io`)"
|
|
||||||
service: polaris
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
dashboard:
|
{{- range $name, $svc := $reef }}
|
||||||
|
{{ $name }}:
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://dashboard:80"
|
- url: "http://{{ $name }}:{{ get $svc "port" }}"
|
||||||
|
{{- end }}
|
||||||
gitea:
|
{{- range $name, $svc := $multi }}
|
||||||
|
{{ $name }}:
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://gitea:3000"
|
- url: "http://{{ get $svc "container" }}:{{ get $svc "port" }}"
|
||||||
|
{{- end }}
|
||||||
woodpecker:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://woodpecker-server:8000"
|
|
||||||
|
|
||||||
notebooks:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://notebooks:2718"
|
|
||||||
|
|
||||||
zotero:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://zotero:8080"
|
|
||||||
|
|
||||||
nessie:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://nessie:19120"
|
|
||||||
|
|
||||||
trino:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://trino:8080"
|
|
||||||
|
|
||||||
rustfs-api:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://rustfs:9000"
|
|
||||||
|
|
||||||
rustfs-console:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://rustfs:9001"
|
|
||||||
|
|
||||||
grafana:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://grafana:3000"
|
|
||||||
|
|
||||||
prometheus:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://prometheus:9090"
|
|
||||||
|
|
||||||
jaeger:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://jaeger:16686"
|
|
||||||
|
|
||||||
loki:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://loki:3100"
|
|
||||||
|
|
||||||
polaris:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://polaris:8181"
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/go/.devcontainer/base.Dockerfile
|
||||||
|
|
||||||
|
# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.16, 1.17, 1-bullseye, 1.16-bullseye, 1.17-bullseye, 1-buster, 1.16-buster, 1.17-buster
|
||||||
|
ARG VARIANT="1.18-bullseye"
|
||||||
|
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
|
||||||
|
|
||||||
|
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||||
|
ARG NODE_VERSION="none"
|
||||||
|
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||||
|
|
||||||
|
# [Optional] Uncomment this section to install additional OS packages.
|
||||||
|
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
&& apt-get -y install --no-install-recommends \
|
||||||
|
vim
|
||||||
|
|
||||||
|
# [Optional] Uncomment the next lines to use go get to install anything else you need
|
||||||
|
USER vscode
|
||||||
|
RUN go install mvdan.cc/gofumpt@latest
|
||||||
|
|
||||||
|
ENV YAEGI_VERSION="v0.9.20"
|
||||||
|
RUN curl -sfL https://raw.githubusercontent.com/traefik/yaegi/master/install.sh | bash -s -- -b $(go env GOPATH)/bin ${YAEGI_VERSION}
|
||||||
|
|
||||||
|
# [Optional] Uncomment this line to install global node packages.
|
||||||
|
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||||
|
// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/go
|
||||||
|
{
|
||||||
|
"name": "rewrite-body",
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile",
|
||||||
|
"args": {
|
||||||
|
// Update the VARIANT arg to pick a version of Go: 1, 1.18, 1.17
|
||||||
|
// Append -bullseye or -buster to pin to an OS version.
|
||||||
|
// Use -bullseye variants on local arm64/Apple Silicon.
|
||||||
|
"VARIANT": "1.18",
|
||||||
|
// Options
|
||||||
|
"NODE_VERSION": "lts/*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runArgs": [
|
||||||
|
"--cap-add=SYS_PTRACE",
|
||||||
|
"--security-opt",
|
||||||
|
"seccomp=unconfined"
|
||||||
|
],
|
||||||
|
// Set *default* container specific settings.json values on container create.
|
||||||
|
"settings": {
|
||||||
|
"go.toolsManagement.checkForUpdates": "local",
|
||||||
|
"go.useLanguageServer": true,
|
||||||
|
"go.gopath": "/go",
|
||||||
|
"terminal.integrated.defaultProfile.linux": "zsh"
|
||||||
|
},
|
||||||
|
"mounts": [
|
||||||
|
"source=${localWorkspaceFolderBasename}-home,target=/home/vscode,type=volume",
|
||||||
|
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind",
|
||||||
|
"source=${localEnv:HOME}/.zshrc,target=/home/vscode/.zshrc,type=bind",
|
||||||
|
"source=${localEnv:HOME}/.oh-my-zsh,target=/home/vscode/.oh-my-zsh,type=bind",
|
||||||
|
"source=${localEnv:HOME}/.oh-my-zsh-custom,target=/home/vscode/.oh-my-zsh-custom,type=bind",
|
||||||
|
],
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
"golang.Go"
|
||||||
|
],
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "go version",
|
||||||
|
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||||
|
"remoteUser": "vscode",
|
||||||
|
"features": {
|
||||||
|
"git": "latest"
|
||||||
|
},
|
||||||
|
"workspaceMount": "source=${localWorkspaceFolder},target=/go/src/github.com/${localWorkspaceFolderBasename},type=bind,consistency=cached",
|
||||||
|
"workspaceFolder": "/go/src/github.com/${localWorkspaceFolderBasename}"
|
||||||
|
}
|
||||||
44
traefik/plugins/src/github.com/packruler/rewrite-body/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
44
traefik/plugins/src/github.com/packruler/rewrite-body/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Relevant Config**
|
||||||
|
Any specific information about regex used. Specific information about
|
||||||
|
HTTP query would be appreciated as well!
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
|
||||||
|
**Actual behavior**
|
||||||
|
A clear and concise description of what actually happened.
|
||||||
|
|
||||||
|
**Logs**
|
||||||
|
Any logs from your Treafik logs would be greatly appreciated!!
|
||||||
|
|
||||||
|
You can add the following to your Traefik config:
|
||||||
|
```yaml
|
||||||
|
log:
|
||||||
|
level: DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
|
Then look for logs that end with:
|
||||||
|
```console
|
||||||
|
module=github.com/packruler/rewrite-body plugin=plugin-rewritebody
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server (please complete the following information):**
|
||||||
|
- OS: [e.g. Docker, Ubuntu]
|
||||||
|
- Traefik Version [e.g. 2.6]
|
||||||
|
- Plugin Version [e.g. 0.5.0]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
traefik/plugins/src/github.com/packruler/rewrite-body/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
traefik/plugins/src/github.com/packruler/rewrite-body/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
47
traefik/plugins/src/github.com/packruler/rewrite-body/.github/workflows/go-cross.yml
vendored
Normal file
47
traefik/plugins/src/github.com/packruler/rewrite-body/.github/workflows/go-cross.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Go Matrix
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
cross:
|
||||||
|
name: Go
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [ 1.16, 1.17, 1.x ]
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# https://github.com/marketplace/actions/setup-go-environment
|
||||||
|
- name: Set up Go ${{ matrix.go-version }}
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
|
# https://github.com/marketplace/actions/checkout
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# https://github.com/marketplace/actions/cache
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
# In order:
|
||||||
|
# * Module download cache
|
||||||
|
# * Build cache (Linux)
|
||||||
|
# * Build cache (Mac)
|
||||||
|
# * Build cache (Windows)
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
|
~/Library/Caches/go-build
|
||||||
|
%LocalAppData%\go-build
|
||||||
|
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.go-version }}-go-
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v -cover ./...
|
||||||
74
traefik/plugins/src/github.com/packruler/rewrite-body/.github/workflows/main.yml
vendored
Normal file
74
traefik/plugins/src/github.com/packruler/rewrite-body/.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
name: Main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- plugin-utils
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
main:
|
||||||
|
name: Main Process
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GO_VERSION: 1.16
|
||||||
|
GOLANGCI_LINT_VERSION: v1.41.1
|
||||||
|
YAEGI_VERSION: v0.9.20
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{ github.workspace }}/go/src/github.com/${{ github.repository }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
# https://github.com/marketplace/actions/setup-go-environment
|
||||||
|
- name: Set up Go ${{ env.GO_VERSION }}
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
# https://github.com/marketplace/actions/checkout
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
path: go/src/github.com/${{ github.repository }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# https://github.com/marketplace/actions/cache
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
# https://golangci-lint.run/usage/install#other-ci
|
||||||
|
- name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }}
|
||||||
|
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION}
|
||||||
|
|
||||||
|
- name: Install Yaegi ${{ env.YAEGI_VERSION }}
|
||||||
|
run: curl -sfL https://raw.githubusercontent.com/traefik/yaegi/master/install.sh | bash -s -- -b $(go env GOPATH)/bin ${YAEGI_VERSION}
|
||||||
|
|
||||||
|
- name: Setup GOPATH
|
||||||
|
run: go env -w GOPATH=${{ github.workspace }}/go
|
||||||
|
|
||||||
|
- name: Check and get dependencies
|
||||||
|
run: |
|
||||||
|
go mod tidy
|
||||||
|
git diff --exit-code go.mod
|
||||||
|
# git diff --exit-code go.sum
|
||||||
|
go mod vendor
|
||||||
|
# git diff --exit-code ./vendor/
|
||||||
|
|
||||||
|
- name: Lint and Tests
|
||||||
|
run: make
|
||||||
|
|
||||||
|
- name: Run tests with Yaegi
|
||||||
|
run: make yaegi_test
|
||||||
|
env:
|
||||||
|
GOPATH: ${{ github.workspace }}/go
|
||||||
2
traefik/plugins/src/github.com/packruler/rewrite-body/.gitignore
vendored
Normal file
2
traefik/plugins/src/github.com/packruler/rewrite-body/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
run:
|
||||||
|
timeout: 3m
|
||||||
|
skip-files: []
|
||||||
|
skip-dirs: []
|
||||||
|
linters-settings:
|
||||||
|
govet:
|
||||||
|
check-shadowing: false
|
||||||
|
golint:
|
||||||
|
min-confidence: 0
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 12
|
||||||
|
maligned:
|
||||||
|
suggest-new: true
|
||||||
|
goconst:
|
||||||
|
min-len: 3
|
||||||
|
min-occurrences: 4
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
funlen:
|
||||||
|
lines: 60
|
||||||
|
statements: 40
|
||||||
|
ireturn:
|
||||||
|
# ireturn allows using `allow` and `reject` settings at the same time.
|
||||||
|
# Both settings are lists of the keywords and regular expressions matched to interface or package names.
|
||||||
|
# keywords:
|
||||||
|
# - `empty` for `interface{}`
|
||||||
|
# - `error` for errors
|
||||||
|
# - `stdlib` for standard library
|
||||||
|
# - `anon` for anonymous interfaces
|
||||||
|
|
||||||
|
# By default, it allows using errors, empty interfaces, anonymous interfaces,
|
||||||
|
# and interfaces provided by the standard library.
|
||||||
|
allow:
|
||||||
|
- anon
|
||||||
|
- error
|
||||||
|
- empty
|
||||||
|
- stdlib
|
||||||
|
# You can specify idiomatic endings for interface
|
||||||
|
- (or|er)$
|
||||||
|
- github.com/packruler/rewrite-body/logger.LogWriter
|
||||||
|
linters:
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- scopelint
|
||||||
|
- golint
|
||||||
|
- interfacer
|
||||||
|
- maligned
|
||||||
|
- bodyclose
|
||||||
|
- goerr113
|
||||||
|
- wrapcheck
|
||||||
|
- maligned
|
||||||
|
- exhaustivestruct
|
||||||
|
- testpackage
|
||||||
|
- paralleltest
|
||||||
|
- tparallel
|
||||||
|
issues:
|
||||||
|
exclude-use-default: false
|
||||||
|
max-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
exclude:
|
||||||
|
- don't use an underscore in package name
|
||||||
|
- "ST1003: should not use underscores in package names"
|
||||||
|
- "SA1019: http.CloseNotifier has been deprecated since Go 1.11 and an alternative has been available since Go 1.7: the CloseNotifier interface predates Go's context package. New code should use Request.Context instead."
|
||||||
|
exclude-rules:
|
||||||
|
- path: (.+)_test.go
|
||||||
|
linters:
|
||||||
|
- goconst
|
||||||
|
- funlen
|
||||||
|
- godot
|
||||||
|
- lll
|
||||||
|
- path: config.go
|
||||||
|
linters:
|
||||||
|
- lll
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
displayName: Rewrite Body (with compression support)
|
||||||
|
|
||||||
|
summary: Built on top of Traefik Rewrite Body plugin to also support gzip decompress -> update -> recompress.
|
||||||
|
|
||||||
|
type: middleware
|
||||||
|
|
||||||
|
import: github.com/packruler/rewrite-body
|
||||||
|
|
||||||
|
testData:
|
||||||
|
lastModified: true
|
||||||
|
rewrites:
|
||||||
|
- regex: "bar"
|
||||||
|
replacement: "foo"
|
||||||
|
monitor:
|
||||||
|
types:
|
||||||
|
- text/html
|
||||||
|
methods: ["GET"]
|
||||||
202
traefik/plugins/src/github.com/packruler/rewrite-body/LICENCE
Normal file
202
traefik/plugins/src/github.com/packruler/rewrite-body/LICENCE
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2020 Containous SAS
|
||||||
|
Copyright 2020 Traefik Labs
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.PHONY: lint test vendor clean
|
||||||
|
|
||||||
|
export GO111MODULE=on
|
||||||
|
|
||||||
|
default: lint test
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v -cover ./...
|
||||||
|
|
||||||
|
yaegi_test:
|
||||||
|
yaegi test -v .
|
||||||
|
|
||||||
|
vendor:
|
||||||
|
go mod vendor
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf ./vendor
|
||||||
130
traefik/plugins/src/github.com/packruler/rewrite-body/README.md
Normal file
130
traefik/plugins/src/github.com/packruler/rewrite-body/README.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# Rewrite Body
|
||||||
|
|
||||||
|
This is a fork of [Traefik](https://github.com/traefik)'s [plugin-rewritebody](https://github.com/traefik/plugin-rewritebody)
|
||||||
|
that is aimed at extending support to handle `gzip` content. This was initially aimed at extending the support for utilizing
|
||||||
|
[theme.park](https://github.com/traefik/plugin-rewritebody)'s themes but can likely be used for a range of other uses.
|
||||||
|
|
||||||
|
## Changes From Original Traefik Plugin
|
||||||
|
|
||||||
|
The primary change is to add support for `gzip` content. This brought another potential issue to mind, what about really large
|
||||||
|
content? This was handled as well.
|
||||||
|
|
||||||
|
### Process For Handling Body Content
|
||||||
|
|
||||||
|
#### Body Content Requirements
|
||||||
|
|
||||||
|
* The header must have `Content-Type` that includes `text`. For example:
|
||||||
|
* `text/html`
|
||||||
|
* `text/json`
|
||||||
|
* The header must have `Content-Encoding` header that is supported by this plugin
|
||||||
|
* The original plugin supported `Content-Encoding` of `identity` or empty
|
||||||
|
* This plugin adds support for `gzip` and `zlib` encoding
|
||||||
|
|
||||||
|
#### Processing Paths
|
||||||
|
|
||||||
|
* If the either of the previous conditions failes the body is passed on as is and no further processing from this plugin occurs.
|
||||||
|
|
||||||
|
* If the `Content-Encoding` is empty or `identity` it is handled in mostly the same manner as the original plugin.
|
||||||
|
|
||||||
|
* If the `Content-Encoding` is `gzip` the following process happens:
|
||||||
|
* The body content is decompressed by [Go-lang's gzip library](https://pkg.go.dev/compress/gzip)
|
||||||
|
* The resulting content is run through the `regex` process created by the original plugin
|
||||||
|
* The processed content is then compressed with the same library and returned
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Static
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
pilot:
|
||||||
|
token: "xxxx"
|
||||||
|
|
||||||
|
experimental:
|
||||||
|
plugins:
|
||||||
|
rewrite-body:
|
||||||
|
moduleName: "github.com/packruler/rewrite-body"
|
||||||
|
version: "v1.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic
|
||||||
|
|
||||||
|
To configure the `Rewrite Body` plugin you should create a [middleware](https://docs.traefik.io/middlewares/overview/) in
|
||||||
|
your dynamic configuration as explained [here](https://docs.traefik.io/middlewares/overview/). The following example creates
|
||||||
|
and uses the `rewritebody` middleware plugin to replace all foo occurences by bar in the HTTP response body.
|
||||||
|
|
||||||
|
If you want to apply some limits on the response body, you can chain this middleware plugin with the [Buffering middleware](https://docs.traefik.io/middlewares/buffering/) from Traefik.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
routers:
|
||||||
|
my-router:
|
||||||
|
rule: "Host(`localhost`)"
|
||||||
|
middlewares:
|
||||||
|
- "rewrite-foo"
|
||||||
|
service: "my-service"
|
||||||
|
|
||||||
|
middlewares:
|
||||||
|
rewrite-foo:
|
||||||
|
plugin:
|
||||||
|
rewrite-body:
|
||||||
|
# Keep Last-Modified header returned by the HTTP service.
|
||||||
|
# By default, the Last-Modified header is removed.
|
||||||
|
lastModified: true
|
||||||
|
|
||||||
|
# Rewrites all "foo" occurences by "bar"
|
||||||
|
rewrites:
|
||||||
|
- regex: "foo"
|
||||||
|
replacement: "bar"
|
||||||
|
|
||||||
|
# logLevel is optional, defaults to Info level.
|
||||||
|
# Available logLevels: (Trace: -2, Debug: -1, Info: 0, Warning: 1, Error: 2)
|
||||||
|
logLevel: 0
|
||||||
|
|
||||||
|
# monitoring is optional, defaults to below configuration
|
||||||
|
# monitoring configuration limits the HTTP queries that are checked for regex replacement.
|
||||||
|
monitoring:
|
||||||
|
# methods is a string list. Options are standard HTTP Methods. Entries MUST be ALL CAPS
|
||||||
|
# For a list of options: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
||||||
|
methods:
|
||||||
|
- GET
|
||||||
|
# types is a string list. Options are HTTP Content Types. Entries should match standard formatting
|
||||||
|
# For a list of options: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
|
||||||
|
# Wildcards(*) are not supported!
|
||||||
|
types:
|
||||||
|
- text/html
|
||||||
|
services:
|
||||||
|
my-service:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://127.0.0.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example theme.park
|
||||||
|
|
||||||
|
### Dynamic
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
routers:
|
||||||
|
sonarr-router:
|
||||||
|
rule: "Host(`sonarr.example.com`)"
|
||||||
|
middlewares:
|
||||||
|
- sonarr-theme
|
||||||
|
service: sonarr-service
|
||||||
|
|
||||||
|
middlewares:
|
||||||
|
sonarr-theme:
|
||||||
|
plugin:
|
||||||
|
rewrite-body:
|
||||||
|
rewrites:
|
||||||
|
- regex: </head>
|
||||||
|
replacement: <link rel="stylesheet" type="text/css" href="https://theme-park.dev/css/base/sonarr/{{ env "THEME" }}.css"></head>
|
||||||
|
|
||||||
|
services:
|
||||||
|
sonarr-service:
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:8989
|
||||||
|
```
|
||||||
|
|
||||||
|
You can set an environment variable `THEME` to the name of a theme for easier consistency across apps.
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package compressutil
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Gzip compression algorithm string.
|
||||||
|
Gzip string = "gzip"
|
||||||
|
// Deflate compression algorithm string.
|
||||||
|
Deflate string = "deflate"
|
||||||
|
// Identity compression algorithm string.
|
||||||
|
Identity string = "identity"
|
||||||
|
)
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// Package compressutil a plugin to handle compression and decompression tasks
|
||||||
|
package compressutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/flate"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReaderError for notating that an error occurred while reading compressed data.
|
||||||
|
type ReaderError struct {
|
||||||
|
error
|
||||||
|
|
||||||
|
cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode data in a bytes.Reader based on supplied encoding.
|
||||||
|
func Decode(byteReader *bytes.Buffer, encoding string) ([]byte, error) {
|
||||||
|
reader, err := getRawReader(byteReader, encoding)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &ReaderError{
|
||||||
|
error: err,
|
||||||
|
cause: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRawReader(byteReader *bytes.Buffer, encoding string) (io.Reader, error) {
|
||||||
|
switch encoding {
|
||||||
|
case Gzip:
|
||||||
|
return gzip.NewReader(byteReader)
|
||||||
|
|
||||||
|
case Deflate:
|
||||||
|
return flate.NewReader(byteReader), nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return byteReader, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode data in a []byte based on supplied encoding.
|
||||||
|
func Encode(data []byte, encoding string) ([]byte, error) {
|
||||||
|
switch encoding {
|
||||||
|
case Gzip:
|
||||||
|
return compressWithGzip(data)
|
||||||
|
|
||||||
|
case Deflate:
|
||||||
|
return compressWithZlib(data)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressWithGzip(bodyBytes []byte) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
gzipWriter := gzip.NewWriter(&buf)
|
||||||
|
|
||||||
|
if _, err := gzipWriter.Write(bodyBytes); err != nil {
|
||||||
|
log.Printf("unable to recompress rewrited body: %v", err)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gzipWriter.Close(); err != nil {
|
||||||
|
log.Printf("unable to close gzip writer: %v", err)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressWithZlib(bodyBytes []byte) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zlibWriter, _ := flate.NewWriter(&buf, flate.DefaultCompression)
|
||||||
|
|
||||||
|
if _, err := zlibWriter.Write(bodyBytes); err != nil {
|
||||||
|
log.Printf("unable to recompress rewrited body: %v", err)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := zlibWriter.Close(); err != nil {
|
||||||
|
log.Printf("unable to close zlib writer: %v", err)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package compressutil_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/packruler/rewrite-body/compressutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestStruct struct {
|
||||||
|
desc string
|
||||||
|
input []byte
|
||||||
|
expected []byte
|
||||||
|
encoding string
|
||||||
|
shouldMatch bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
var (
|
||||||
|
deflatedBytes = []byte{
|
||||||
|
74, 203, 207, 87, 200, 44, 86, 40, 201, 72, 85,
|
||||||
|
200, 75, 45, 87, 72, 74, 44, 2, 4, 0, 0, 255, 255,
|
||||||
|
}
|
||||||
|
gzippedBytes = []byte{
|
||||||
|
31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 74, 203, 207, 87, 200, 44, 86, 40, 201, 72, 85,
|
||||||
|
200, 75, 45, 87, 72, 74, 44, 2, 4, 0, 0, 255, 255, 251, 28, 166, 187, 18, 0, 0, 0,
|
||||||
|
}
|
||||||
|
normalBytes = []byte("foo is the new bar")
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := []TestStruct{
|
||||||
|
{
|
||||||
|
desc: "should support identity",
|
||||||
|
input: normalBytes,
|
||||||
|
expected: normalBytes,
|
||||||
|
encoding: compressutil.Identity,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should support gzip",
|
||||||
|
input: normalBytes,
|
||||||
|
expected: gzippedBytes,
|
||||||
|
encoding: compressutil.Gzip,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should support deflate",
|
||||||
|
input: normalBytes,
|
||||||
|
expected: deflatedBytes,
|
||||||
|
encoding: compressutil.Deflate,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should NOT support brotli",
|
||||||
|
input: normalBytes,
|
||||||
|
expected: normalBytes,
|
||||||
|
encoding: "br",
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
output, err := compressutil.Encode(test.input, test.encoding)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isBad := !bytes.Equal(test.expected, output)
|
||||||
|
|
||||||
|
if isBad {
|
||||||
|
t.Errorf("expected error got body: %v\n wanted: %v", output, test.expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.shouldMatch {
|
||||||
|
isBad = !bytes.Equal(test.input, output)
|
||||||
|
} else {
|
||||||
|
isBad = bytes.Equal(test.input, output)
|
||||||
|
}
|
||||||
|
if isBad {
|
||||||
|
t.Errorf("match error got body: %v\n wanted: %v", output, test.input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
var (
|
||||||
|
deflatedBytes = []byte{
|
||||||
|
74, 203, 207, 87, 200, 44, 86, 40, 201, 72, 85,
|
||||||
|
200, 75, 45, 87, 72, 74, 44, 2, 4, 0, 0, 255, 255,
|
||||||
|
}
|
||||||
|
gzippedBytes = []byte{
|
||||||
|
31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 74, 203, 207, 87, 200, 44, 86, 40, 201, 72, 85,
|
||||||
|
200, 75, 45, 87, 72, 74, 44, 2, 4, 0, 0, 255, 255, 251, 28, 166, 187, 18, 0, 0, 0,
|
||||||
|
}
|
||||||
|
normalBytes = []byte("foo is the new bar")
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := []TestStruct{
|
||||||
|
{
|
||||||
|
desc: "should support identity",
|
||||||
|
input: normalBytes,
|
||||||
|
expected: normalBytes,
|
||||||
|
encoding: compressutil.Identity,
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should support gzip",
|
||||||
|
input: gzippedBytes,
|
||||||
|
expected: normalBytes,
|
||||||
|
encoding: compressutil.Gzip,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should support deflate",
|
||||||
|
input: deflatedBytes,
|
||||||
|
expected: normalBytes,
|
||||||
|
encoding: compressutil.Deflate,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should NOT support brotli",
|
||||||
|
input: normalBytes,
|
||||||
|
expected: normalBytes,
|
||||||
|
encoding: "br",
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
output, err := compressutil.Decode(bytes.NewBuffer(test.input), test.encoding)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isBad := !bytes.Equal(test.expected, output)
|
||||||
|
|
||||||
|
if isBad {
|
||||||
|
t.Errorf("expected error got body: %v\n wanted: %v", output, test.expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.shouldMatch {
|
||||||
|
isBad = !bytes.Equal(test.input, output)
|
||||||
|
} else {
|
||||||
|
isBad = bytes.Equal(test.input, output)
|
||||||
|
}
|
||||||
|
if isBad {
|
||||||
|
t.Errorf("match error got body: %s\n wanted: %s", output, test.input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[config]
|
||||||
|
lastModified = true
|
||||||
|
logLevel = 0
|
||||||
|
[[config.rewrites]]
|
||||||
|
regex = "test"
|
||||||
|
replacement = "test"
|
||||||
|
[config.monitor]
|
||||||
|
types = ["text/html"]
|
||||||
|
methods = ["GET"]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
config:
|
||||||
|
lastModified: true
|
||||||
|
logLevel: 0
|
||||||
|
monitor:
|
||||||
|
types:
|
||||||
|
- text/html
|
||||||
|
methods: ["GET"]
|
||||||
|
rewrites:
|
||||||
|
- regex: "test"
|
||||||
|
replacement: "test"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/packruler/rewrite-body
|
||||||
|
|
||||||
|
go 1.16
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/packruler/rewrite-body/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rewrite holds one rewrite body configuration.
|
||||||
|
type Rewrite struct {
|
||||||
|
Regex string `json:"regex" yaml:"regex" toml:"regex"`
|
||||||
|
Replacement string `json:"replacement" yaml:"replacement" toml:"replacement"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds the plugin configuration.
|
||||||
|
type Config struct {
|
||||||
|
LastModified bool `json:"lastModified" toml:"lastModified" yaml:"lastModified"`
|
||||||
|
Rewrites []Rewrite `json:"rewrites" toml:"rewrites" yaml:"rewrites"`
|
||||||
|
LogLevel int8 `json:"logLevel" toml:"logLevel" yaml:"logLevel"`
|
||||||
|
Monitoring httputil.MonitoringConfig `json:"monitoring" toml:"monitoring" yaml:"monitoring"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type rewrite struct {
|
||||||
|
regex *regexp.Regexp
|
||||||
|
replacement []byte
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// Package handler a plugin to rewrite response body.
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/packruler/rewrite-body/httputil"
|
||||||
|
"github.com/packruler/rewrite-body/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rewriteBody struct {
|
||||||
|
name string
|
||||||
|
next http.Handler
|
||||||
|
rewrites []rewrite
|
||||||
|
lastModified bool
|
||||||
|
logger logger.LogWriter
|
||||||
|
monitoringConfig httputil.MonitoringConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates and returns a new rewrite body plugin instance.
|
||||||
|
func New(_ context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
|
||||||
|
rewrites := make([]rewrite, len(config.Rewrites))
|
||||||
|
|
||||||
|
for index, rewriteConfig := range config.Rewrites {
|
||||||
|
regex, err := regexp.Compile(rewriteConfig.Regex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error compiling regex %q: %w", rewriteConfig.Regex, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrites[index] = rewrite{
|
||||||
|
regex: regex,
|
||||||
|
replacement: []byte(rewriteConfig.Replacement),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logWriter := *logger.CreateLogger(logger.LogLevel(config.LogLevel))
|
||||||
|
|
||||||
|
config.Monitoring.EnsureDefaults()
|
||||||
|
config.Monitoring.EnsureProperFormat()
|
||||||
|
|
||||||
|
result := &rewriteBody{
|
||||||
|
name: name,
|
||||||
|
next: next,
|
||||||
|
rewrites: rewrites,
|
||||||
|
lastModified: config.LastModified,
|
||||||
|
logger: logWriter,
|
||||||
|
monitoringConfig: config.Monitoring,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(config)
|
||||||
|
|
||||||
|
logWriter.LogDebugf("Initial config: %v", string(data))
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bodyRewrite *rewriteBody) ServeHTTP(response http.ResponseWriter, req *http.Request) {
|
||||||
|
defer bodyRewrite.handlePanic()
|
||||||
|
|
||||||
|
wrappedRequest := httputil.WrapRequest(req, bodyRewrite.monitoringConfig, bodyRewrite.logger)
|
||||||
|
// allow default http.ResponseWriter to handle calls targeting WebSocket upgrades and non GET methods
|
||||||
|
if !wrappedRequest.SupportsProcessing() {
|
||||||
|
bodyRewrite.logger.LogDebugf("Ignoring unsupported request: %v", req)
|
||||||
|
bodyRewrite.next.ServeHTTP(response, req)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyRewrite.logger.LogDebugf("Starting supported request: %v", req)
|
||||||
|
|
||||||
|
wrappedWriter := httputil.WrapWriter(
|
||||||
|
response,
|
||||||
|
bodyRewrite.monitoringConfig,
|
||||||
|
bodyRewrite.logger,
|
||||||
|
bodyRewrite.lastModified,
|
||||||
|
)
|
||||||
|
|
||||||
|
wrappedWriter.SetLastModified(bodyRewrite.lastModified)
|
||||||
|
|
||||||
|
// look into using https://pkg.go.dev/net/http#RoundTripper
|
||||||
|
bodyRewrite.next.ServeHTTP(wrappedWriter, wrappedRequest.CloneWithSupportedEncoding())
|
||||||
|
|
||||||
|
if !wrappedWriter.SupportsProcessing() {
|
||||||
|
// We are ignoring these any errors because the content should be unchanged here.
|
||||||
|
// This could "error" if writing is not supported but content will return properly.
|
||||||
|
_, _ = response.Write(wrappedWriter.GetBuffer().Bytes())
|
||||||
|
bodyRewrite.logger.LogDebugf("Ignoring unsupported response: %v", wrappedWriter)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := wrappedWriter.GetContent()
|
||||||
|
if err != nil {
|
||||||
|
bodyRewrite.logger.LogErrorf("Error loading content: %v", err)
|
||||||
|
|
||||||
|
if _, err := response.Write(wrappedWriter.GetBuffer().Bytes()); err != nil {
|
||||||
|
bodyRewrite.logger.LogErrorf("unable to write error content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyRewrite.logger.LogDebugf("Response body: %s", bodyBytes)
|
||||||
|
|
||||||
|
if len(bodyBytes) == 0 {
|
||||||
|
// If the body is empty there is no purpose in continuing this process.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rwt := range bodyRewrite.rewrites {
|
||||||
|
bodyBytes = rwt.regex.ReplaceAll(bodyBytes, rwt.replacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyRewrite.logger.LogDebugf("Transformed body: %s", bodyBytes)
|
||||||
|
|
||||||
|
encoding := wrappedWriter.Header().Get("Content-Encoding")
|
||||||
|
wrappedWriter.SetContent(bodyBytes, encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bodyRewrite *rewriteBody) handlePanic() {
|
||||||
|
if recovery := recover(); recovery != nil {
|
||||||
|
if err, ok := recovery.(error); ok {
|
||||||
|
bodyRewrite.logError(err)
|
||||||
|
} else {
|
||||||
|
bodyRewrite.logger.LogWarningf("Unhandled error: %v", recovery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bodyRewrite *rewriteBody) logError(err error) {
|
||||||
|
// Ignore http.ErrAbortHandler because they are expected errors that do not require handling
|
||||||
|
if errors.Is(err, http.ErrAbortHandler) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyRewrite.logger.LogWarningf("Recovered from: %v", err)
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/packruler/rewrite-body/compressutil"
|
||||||
|
"github.com/packruler/rewrite-body/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServeHTTP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
contentEncoding string
|
||||||
|
contentType string `default:"text/html"`
|
||||||
|
rewrites []Rewrite
|
||||||
|
lastModified bool
|
||||||
|
resBody string
|
||||||
|
expResBody string
|
||||||
|
expLastModified bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "should replace foo by bar",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentType: "text/html",
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "bar is the new bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should replace foo by bar, then by foo",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Regex: "bar",
|
||||||
|
Replacement: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentType: "text/html",
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "foo is the new foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should not replace anything if content encoding is not identity or empty",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "other",
|
||||||
|
contentType: "text/html",
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "foo is the new bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should not replace anything if content type does not contain text or is not empty",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentType: "image",
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "foo is the new bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should replace foo by bar if content encoding is identity",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "identity",
|
||||||
|
contentType: "text/html",
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "bar is the new bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should not remove the last modified header",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "identity",
|
||||||
|
contentType: "text/html",
|
||||||
|
lastModified: true,
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "bar is the new bar",
|
||||||
|
expLastModified: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should support gzip encoding",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "gzip",
|
||||||
|
contentType: "text/html",
|
||||||
|
lastModified: true,
|
||||||
|
resBody: compressString("foo is the new bar", "gzip"),
|
||||||
|
expResBody: compressString("bar is the new bar", "gzip"),
|
||||||
|
expLastModified: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should support deflate encoding",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "deflate",
|
||||||
|
contentType: "text/html",
|
||||||
|
lastModified: true,
|
||||||
|
resBody: compressString("foo is the new bar", "deflate"),
|
||||||
|
expResBody: compressString("bar is the new bar", "deflate"),
|
||||||
|
expLastModified: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should ignore unsupported encoding",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "br",
|
||||||
|
contentType: "text/html",
|
||||||
|
lastModified: true,
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "foo is the new bar",
|
||||||
|
expLastModified: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
LastModified: test.lastModified,
|
||||||
|
Rewrites: test.rewrites,
|
||||||
|
LogLevel: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
next := func(responseWriter http.ResponseWriter, req *http.Request) {
|
||||||
|
responseWriter.Header().Set("Content-Encoding", test.contentEncoding)
|
||||||
|
responseWriter.Header().Set("Content-Type", test.contentType)
|
||||||
|
responseWriter.Header().Set("Last-Modified", "Thu, 02 Jun 2016 06:01:08 GMT")
|
||||||
|
responseWriter.Header().Set("Content-Length", strconv.Itoa(len(test.resBody)))
|
||||||
|
responseWriter.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(responseWriter, test.resBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
rewriteBody, err := New(context.Background(), http.HandlerFunc(next), config, "rewriteBody")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Set("Accept", "text/html")
|
||||||
|
|
||||||
|
rewriteBody.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
if _, exists := recorder.Result().Header["Last-Modified"]; exists != test.expLastModified {
|
||||||
|
t.Errorf("got last-modified header %v, want %v", exists, test.expLastModified)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := recorder.Result().Header["Content-Length"]; exists {
|
||||||
|
t.Error("The Content-Length Header must be deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal([]byte(test.expResBody), recorder.Body.Bytes()) {
|
||||||
|
t.Errorf("got body: %v\n wanted: %v", recorder.Body.Bytes(), []byte(test.expResBody))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressString(value string, encoding string) string {
|
||||||
|
compressed, _ := compressutil.Encode([]byte(value), encoding)
|
||||||
|
|
||||||
|
return string(compressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
rewrites []Rewrite
|
||||||
|
expErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "should return no error",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Regex: "bar",
|
||||||
|
Replacement: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return an error",
|
||||||
|
rewrites: []Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "*",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMonitoring := httputil.MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{http.MethodGet},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Rewrites: test.rewrites,
|
||||||
|
Monitoring: defaultMonitoring,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := New(context.Background(), nil, config, "rewriteBody")
|
||||||
|
if test.expErr && err == nil {
|
||||||
|
t.Fatal("expected error on bad regexp format")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MonitoringConfig structure of data for handling configuration for
|
||||||
|
// controlling what content is monitored.
|
||||||
|
type MonitoringConfig struct {
|
||||||
|
Types []string `json:"types,omitempty" yaml:"types,omitempty" toml:"types,omitempty" export:"true"`
|
||||||
|
Methods []string `json:"methods,omitempty" yaml:"methods,omitempty" toml:"methods,omitempty" export:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureDefaults check Types and Methods for empty arrays and apply default values if found.
|
||||||
|
func (config *MonitoringConfig) EnsureDefaults() {
|
||||||
|
if len(config.Methods) == 0 {
|
||||||
|
config.Methods = []string{http.MethodGet}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Types) == 0 {
|
||||||
|
config.Types = []string{"text/html"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureProperFormat handle weird yaml parsing until the underlying issue can be resolved.
|
||||||
|
func (config *MonitoringConfig) EnsureProperFormat() {
|
||||||
|
if len(config.Methods) == 1 && strings.HasPrefix(config.Methods[0], "║24║") {
|
||||||
|
config.Methods = strings.Split(strings.ReplaceAll(config.Methods[0], "║24║", ""), "║")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Types) == 1 && strings.HasPrefix(config.Types[0], "║24║") {
|
||||||
|
config.Types = strings.Split(strings.ReplaceAll(config.Types[0], "║24║", ""), "║")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package httputil_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/packruler/rewrite-body/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMonitoringConfigParsing(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
inputConfig httputil.MonitoringConfig
|
||||||
|
expectedConfig httputil.MonitoringConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "defaults will be supplied for empty arrays",
|
||||||
|
inputConfig: httputil.MonitoringConfig{
|
||||||
|
Types: []string{},
|
||||||
|
Methods: []string{},
|
||||||
|
},
|
||||||
|
expectedConfig: httputil.MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{http.MethodGet},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "defaults will be supplied for empty types with populated methods",
|
||||||
|
inputConfig: httputil.MonitoringConfig{
|
||||||
|
Types: []string{},
|
||||||
|
Methods: []string{"POST"},
|
||||||
|
},
|
||||||
|
expectedConfig: httputil.MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{http.MethodPost},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "defaults will be supplied for populated types with empty methods",
|
||||||
|
inputConfig: httputil.MonitoringConfig{
|
||||||
|
Types: []string{"application/json"},
|
||||||
|
Methods: []string{},
|
||||||
|
},
|
||||||
|
expectedConfig: httputil.MonitoringConfig{
|
||||||
|
Types: []string{"application/json"},
|
||||||
|
Methods: []string{http.MethodGet},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "handle weird yaml parsing",
|
||||||
|
inputConfig: httputil.MonitoringConfig{
|
||||||
|
Types: []string{"║24║application/javascript║application/json"},
|
||||||
|
Methods: []string{},
|
||||||
|
},
|
||||||
|
expectedConfig: httputil.MonitoringConfig{
|
||||||
|
Types: []string{"application/javascript", "application/json"},
|
||||||
|
Methods: []string{http.MethodGet},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
config := test.inputConfig
|
||||||
|
|
||||||
|
config.EnsureDefaults()
|
||||||
|
config.EnsureProperFormat()
|
||||||
|
|
||||||
|
if len(config.Types) != len(test.expectedConfig.Types) {
|
||||||
|
t.Errorf("Expected Types: '%v' | Got Types: '%v'", test.expectedConfig.Types, config.Types)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range test.expectedConfig.Types {
|
||||||
|
if v != config.Types[i] {
|
||||||
|
t.Errorf("Expected Types: '%v' | Got Types: '%v'", test.expectedConfig.Types, config.Types)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Methods) != len(test.expectedConfig.Methods) {
|
||||||
|
t.Errorf("Expected Methods: '%v' | Got Methods: '%v'", test.expectedConfig.Methods, config.Methods)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range test.expectedConfig.Methods {
|
||||||
|
if v != config.Methods[i] {
|
||||||
|
t.Errorf("Expected Methods: '%v' | Got Methods: '%v'", test.expectedConfig.Methods, config.Methods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/packruler/rewrite-body/compressutil"
|
||||||
|
"github.com/packruler/rewrite-body/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequestWrapper a struct that centralizes request modifications.
|
||||||
|
type RequestWrapper struct {
|
||||||
|
logWriter logger.LogWriter
|
||||||
|
monitoring MonitoringConfig
|
||||||
|
|
||||||
|
http.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapRequest to get a new instance of RequestWrapper.
|
||||||
|
func WrapRequest(request *http.Request, monitoringConfig MonitoringConfig, logWriter logger.LogWriter) *RequestWrapper {
|
||||||
|
return &RequestWrapper{
|
||||||
|
logWriter: logWriter,
|
||||||
|
monitoring: monitoringConfig,
|
||||||
|
Request: *request,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneNoEncode create an http.Request that request no encoding.
|
||||||
|
func (req *RequestWrapper) CloneNoEncode() *http.Request {
|
||||||
|
clonedRequest := req.Clone(req.Context())
|
||||||
|
|
||||||
|
clonedRequest.Header.Set("Accept-Encoding", compressutil.Identity)
|
||||||
|
|
||||||
|
return clonedRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneWithSupportedEncoding create an http.Request that request only supported encoding.
|
||||||
|
func (req *RequestWrapper) CloneWithSupportedEncoding() *http.Request {
|
||||||
|
clonedRequest := req.Clone(req.Context())
|
||||||
|
|
||||||
|
clonedRequest.Header.Set("Accept-Encoding", removeUnsupportedAcceptEncoding(clonedRequest.Header))
|
||||||
|
|
||||||
|
return clonedRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEncodingTarget get the supported encoding algorithm preferred by request.
|
||||||
|
func (req *RequestWrapper) GetEncodingTarget() string {
|
||||||
|
// Limit Accept-Encoding header to encodings we can handle.
|
||||||
|
// acceptEncoding := header.ParseAccept(req.Header, "Accept-Encoding")
|
||||||
|
acceptEncoding := parseAcceptEncoding(req.Header)
|
||||||
|
filteredEncodings := make([]encodingSpec, 0, len(acceptEncoding))
|
||||||
|
|
||||||
|
for _, a := range acceptEncoding {
|
||||||
|
switch a.Value {
|
||||||
|
case compressutil.Gzip, compressutil.Deflate:
|
||||||
|
filteredEncodings = append(filteredEncodings, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filteredEncodings) == 0 {
|
||||||
|
return compressutil.Identity
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(filteredEncodings, func(i, j int) bool {
|
||||||
|
return filteredEncodings[i].Quality > filteredEncodings[j].Quality
|
||||||
|
})
|
||||||
|
|
||||||
|
return filteredEncodings[0].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type encodingSpec struct {
|
||||||
|
Value string
|
||||||
|
Quality float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAcceptEncoding(header http.Header) []encodingSpec {
|
||||||
|
encodingHeader := header.Get("Accept-Encoding")
|
||||||
|
if encodingHeader == "*" {
|
||||||
|
return []encodingSpec{{Quality: 1.0, Value: compressutil.Gzip}, {Quality: 1.0, Value: compressutil.Deflate}}
|
||||||
|
}
|
||||||
|
|
||||||
|
encodingList := strings.Split(encodingHeader, ",")
|
||||||
|
result := make([]encodingSpec, 0, len(encodingList))
|
||||||
|
|
||||||
|
for _, encoding := range encodingList {
|
||||||
|
result = append(result, parseEncodingItem(encoding))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEncodingItem(encoding string) encodingSpec {
|
||||||
|
encoding = strings.TrimSpace(encoding)
|
||||||
|
if encoding == "*" {
|
||||||
|
return encodingSpec{Value: compressutil.Gzip, Quality: 1.0}
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.Split(encoding, ";q=")
|
||||||
|
quality := 1.0
|
||||||
|
|
||||||
|
if qualitySplitSize := 2; len(split) == qualitySplitSize {
|
||||||
|
targetFloat := 64
|
||||||
|
|
||||||
|
parsedQuality, err := strconv.ParseFloat(split[1], targetFloat)
|
||||||
|
if err == nil {
|
||||||
|
quality = parsedQuality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodingSpec{Value: split[0], Quality: quality}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeUnsupportedAcceptEncoding(header http.Header) string {
|
||||||
|
encodingList := strings.Split(header.Get("Accept-Encoding"), ",")
|
||||||
|
result := make([]string, 0, len(encodingList))
|
||||||
|
|
||||||
|
for _, encoding := range encodingList {
|
||||||
|
split := strings.Split(strings.TrimSpace(encoding), ";q=")
|
||||||
|
switch split[0] {
|
||||||
|
case compressutil.Gzip, compressutil.Deflate, compressutil.Identity:
|
||||||
|
result = append(result, encoding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(result, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsProcessing determine if http.Request is supported by this plugin.
|
||||||
|
func (req *RequestWrapper) SupportsProcessing() bool {
|
||||||
|
acceptHeader := req.Header.Get("Accept")
|
||||||
|
isSupported := false
|
||||||
|
|
||||||
|
for _, monitoredType := range req.monitoring.Types {
|
||||||
|
if strings.Contains(acceptHeader, monitoredType) {
|
||||||
|
isSupported = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isSupported {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
isSupported = false
|
||||||
|
|
||||||
|
// Ignore non GET requests
|
||||||
|
for _, monitoredMethod := range req.monitoring.Methods {
|
||||||
|
if strings.Contains(req.Method, monitoredMethod) {
|
||||||
|
isSupported = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isSupported {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(req.Header.Get("Upgrade"), "websocket") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/packruler/rewrite-body/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetEncodingTarget(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
acceptEncoding string
|
||||||
|
expectedTarget string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Supports gzip",
|
||||||
|
acceptEncoding: "gzip",
|
||||||
|
expectedTarget: "gzip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Supports deflate",
|
||||||
|
acceptEncoding: "deflate",
|
||||||
|
expectedTarget: "deflate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Supports identity",
|
||||||
|
acceptEncoding: "identity",
|
||||||
|
expectedTarget: "identity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Ignores brotli",
|
||||||
|
acceptEncoding: "br, gzip",
|
||||||
|
expectedTarget: "gzip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Wildcard to gzip",
|
||||||
|
acceptEncoding: "*",
|
||||||
|
expectedTarget: "gzip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Respects quality in order",
|
||||||
|
acceptEncoding: "gzip;q=0.8, deflate;q=0.6",
|
||||||
|
expectedTarget: "gzip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Respects quality out of order",
|
||||||
|
acceptEncoding: "gzip;q=0.8, deflate;q=0.9",
|
||||||
|
expectedTarget: "deflate",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMonitoring := MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{"GET"},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultLogWriter := logger.CreateLogger(logger.Error)
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
request, err := http.NewRequestWithContext(
|
||||||
|
context.Background(),
|
||||||
|
http.MethodGet,
|
||||||
|
"http://google.com",
|
||||||
|
&bytes.Reader{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating request: %v", err)
|
||||||
|
}
|
||||||
|
request.Header.Set("Accept-Encoding", test.acceptEncoding)
|
||||||
|
|
||||||
|
wrappedRequest := WrapRequest(request, defaultMonitoring, *defaultLogWriter)
|
||||||
|
target := wrappedRequest.GetEncodingTarget()
|
||||||
|
if target != test.expectedTarget {
|
||||||
|
t.Errorf("Expected: '%s' | Got: '%s'", test.expectedTarget, target)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveUnuspportedEncoding(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
acceptEncoding string
|
||||||
|
expectedTarget string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Supports gzip",
|
||||||
|
acceptEncoding: "gzip",
|
||||||
|
expectedTarget: "gzip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Supports deflate",
|
||||||
|
acceptEncoding: "deflate",
|
||||||
|
expectedTarget: "deflate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Supports identity",
|
||||||
|
acceptEncoding: "identity",
|
||||||
|
expectedTarget: "identity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Ignores brotli",
|
||||||
|
acceptEncoding: "br, gzip",
|
||||||
|
expectedTarget: " gzip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Wildcard is dropped",
|
||||||
|
acceptEncoding: "*",
|
||||||
|
expectedTarget: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Respects quality in order",
|
||||||
|
acceptEncoding: "gzip;q=0.8, deflate;q=0.6",
|
||||||
|
expectedTarget: "gzip;q=0.8, deflate;q=0.6",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMonitoring := MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{"GET"},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultLogWriter := logger.CreateLogger(logger.Error)
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
request, err := http.NewRequestWithContext(
|
||||||
|
context.Background(),
|
||||||
|
http.MethodGet,
|
||||||
|
"http://google.com",
|
||||||
|
&bytes.Reader{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating request: %v", err)
|
||||||
|
}
|
||||||
|
request.Header.Set("Accept-Encoding", test.acceptEncoding)
|
||||||
|
|
||||||
|
wrappedRequest := WrapRequest(request, defaultMonitoring, *defaultLogWriter)
|
||||||
|
target := wrappedRequest.CloneWithSupportedEncoding().Header.Get("Accept-Encoding")
|
||||||
|
|
||||||
|
if target != test.expectedTarget {
|
||||||
|
t.Errorf("Expected: '%s' | Got: '%s'", test.expectedTarget, target)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSupportsProcessing(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
inputType string
|
||||||
|
inputMethod string
|
||||||
|
monitoringConfig MonitoringConfig
|
||||||
|
expectedSupport bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Supports default config",
|
||||||
|
inputType: "text/html",
|
||||||
|
inputMethod: http.MethodGet,
|
||||||
|
expectedSupport: true,
|
||||||
|
monitoringConfig: MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{"GET"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Supports default browser load",
|
||||||
|
inputType: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||||
|
inputMethod: http.MethodGet,
|
||||||
|
expectedSupport: true,
|
||||||
|
monitoringConfig: MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{"GET"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Supports when types includes unsupported type first",
|
||||||
|
inputType: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||||
|
inputMethod: http.MethodGet,
|
||||||
|
expectedSupport: true,
|
||||||
|
monitoringConfig: MonitoringConfig{
|
||||||
|
Types: []string{"application/javascript", "text/html"},
|
||||||
|
Methods: []string{"GET"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Supports multiple methods",
|
||||||
|
inputType: "text/html",
|
||||||
|
inputMethod: http.MethodPost,
|
||||||
|
expectedSupport: true,
|
||||||
|
monitoringConfig: MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Does not support type not included",
|
||||||
|
inputType: "application/javascript",
|
||||||
|
inputMethod: http.MethodGet,
|
||||||
|
expectedSupport: false,
|
||||||
|
monitoringConfig: MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{"GET"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Does not support method not included",
|
||||||
|
inputType: "text/html",
|
||||||
|
inputMethod: http.MethodPost,
|
||||||
|
expectedSupport: false,
|
||||||
|
monitoringConfig: MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{"GET"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultLogWriter := logger.CreateLogger(logger.Error)
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
request, err := http.NewRequestWithContext(
|
||||||
|
context.Background(),
|
||||||
|
test.inputMethod,
|
||||||
|
"http://google.com",
|
||||||
|
&bytes.Reader{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating request: %v", err)
|
||||||
|
}
|
||||||
|
request.Header.Set("Accept", test.inputType)
|
||||||
|
|
||||||
|
wrappedRequest := WrapRequest(request, test.monitoringConfig, *defaultLogWriter)
|
||||||
|
|
||||||
|
if test.expectedSupport != wrappedRequest.SupportsProcessing() {
|
||||||
|
t.Errorf("Test input: '%v'", test)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
// Package httputil a package for handling http data tasks
|
||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/packruler/rewrite-body/compressutil"
|
||||||
|
"github.com/packruler/rewrite-body/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResponseWrapper a wrapper used to simplify ResponseWriter data access and manipulation.
|
||||||
|
type ResponseWrapper struct {
|
||||||
|
buffer bytes.Buffer
|
||||||
|
lastModified bool `default:"true"`
|
||||||
|
wroteHeader bool
|
||||||
|
|
||||||
|
code int `default:"200"`
|
||||||
|
|
||||||
|
logWriter logger.LogWriter
|
||||||
|
monitoring MonitoringConfig
|
||||||
|
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapWriter create a ResponseWrapper for provided configuration.
|
||||||
|
func WrapWriter(
|
||||||
|
responseWriter http.ResponseWriter,
|
||||||
|
monitoringConfig MonitoringConfig,
|
||||||
|
logWriter logger.LogWriter,
|
||||||
|
lastModified bool,
|
||||||
|
) *ResponseWrapper {
|
||||||
|
return &ResponseWrapper{
|
||||||
|
buffer: bytes.Buffer{},
|
||||||
|
lastModified: lastModified,
|
||||||
|
wroteHeader: false,
|
||||||
|
code: http.StatusOK,
|
||||||
|
logWriter: logWriter,
|
||||||
|
monitoring: monitoringConfig,
|
||||||
|
ResponseWriter: responseWriter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader into wrapped ResponseWriter.
|
||||||
|
func (wrapper *ResponseWrapper) WriteHeader(statusCode int) {
|
||||||
|
if wrapper.wroteHeader {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !wrapper.lastModified {
|
||||||
|
wrapper.ResponseWriter.Header().Del("Last-Modified")
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.code = statusCode
|
||||||
|
wrapper.wroteHeader = true
|
||||||
|
|
||||||
|
// Delegates the Content-Length Header creation to the final body write.
|
||||||
|
wrapper.ResponseWriter.Header().Del("Content-Length")
|
||||||
|
|
||||||
|
wrapper.ResponseWriter.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write data to internal buffer and mark the status code as http.StatusOK.
|
||||||
|
func (wrapper *ResponseWrapper) Write(data []byte) (int, error) {
|
||||||
|
if !wrapper.wroteHeader {
|
||||||
|
wrapper.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper.buffer.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuffer get a pointer to the ResponseWriter buffer.
|
||||||
|
func (wrapper *ResponseWrapper) GetBuffer() *bytes.Buffer {
|
||||||
|
return &wrapper.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContent load the content currently in the internal buffer
|
||||||
|
// accounting for possible encoding.
|
||||||
|
func (wrapper *ResponseWrapper) GetContent() ([]byte, error) {
|
||||||
|
encoding := wrapper.getContentEncoding()
|
||||||
|
|
||||||
|
return compressutil.Decode(wrapper.GetBuffer(), encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContent write data to the internal ResponseWriter buffer
|
||||||
|
// and match initial encoding.
|
||||||
|
func (wrapper *ResponseWrapper) SetContent(data []byte, encoding string) {
|
||||||
|
bodyBytes, _ := compressutil.Encode(data, encoding)
|
||||||
|
|
||||||
|
if !wrapper.wroteHeader {
|
||||||
|
wrapper.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := wrapper.ResponseWriter.Write(bodyBytes); err != nil {
|
||||||
|
wrapper.logWriter.LogErrorf("unable to write rewriten body: %v", err)
|
||||||
|
wrapper.LogHeaders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wrapper *ResponseWrapper) getHeader(headerName string) string {
|
||||||
|
return wrapper.ResponseWriter.Header().Get(headerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogHeaders writes current response headers.
|
||||||
|
func (wrapper *ResponseWrapper) LogHeaders() {
|
||||||
|
wrapper.logWriter.LogDebugf("Error Headers: %v", wrapper.ResponseWriter.Header())
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContentEncoding get the Content-Encoding header value.
|
||||||
|
func (wrapper *ResponseWrapper) getContentEncoding() string {
|
||||||
|
return wrapper.getHeader("Content-Encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContentType get the Content-Encoding header value.
|
||||||
|
func (wrapper *ResponseWrapper) getContentType() string {
|
||||||
|
return wrapper.getHeader("Content-Type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsProcessing determine if HttpWrapper is supported by this plugin based on encoding.
|
||||||
|
func (wrapper *ResponseWrapper) SupportsProcessing() bool {
|
||||||
|
foundContentType := false
|
||||||
|
|
||||||
|
// If content type does not match return values with false
|
||||||
|
contentType := wrapper.getContentType()
|
||||||
|
for _, monitoredType := range wrapper.monitoring.Types {
|
||||||
|
if strings.Contains(contentType, monitoredType) {
|
||||||
|
foundContentType = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundContentType {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
encoding := wrapper.getContentEncoding()
|
||||||
|
|
||||||
|
// If content type is supported validate encoding as well
|
||||||
|
switch encoding {
|
||||||
|
case compressutil.Gzip, compressutil.Deflate, compressutil.Identity, "":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLastModified update the local lastModified variable from non-package-based users.
|
||||||
|
func (wrapper *ResponseWrapper) SetLastModified(value bool) {
|
||||||
|
wrapper.lastModified = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseNotify returns a channel that receives at most a
|
||||||
|
// single value (true) when the client connection has gone away.
|
||||||
|
func (wrapper *ResponseWrapper) CloseNotify() <-chan bool {
|
||||||
|
if w, ok := wrapper.ResponseWriter.(http.CloseNotifier); ok {
|
||||||
|
return w.CloseNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
return make(<-chan bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hijack hijacks the connection.
|
||||||
|
func (wrapper *ResponseWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
if hj, ok := wrapper.ResponseWriter.(http.Hijacker); ok {
|
||||||
|
return hj.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, fmt.Errorf("%T is not a http.Hijacker", wrapper.ResponseWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush sends any buffered data to the client.
|
||||||
|
func (wrapper *ResponseWrapper) Flush() {
|
||||||
|
// If WriteHeader was already called from the caller, this is a NOOP.
|
||||||
|
// Otherwise, codeCatcher.code is actually a 200 here.
|
||||||
|
wrapper.WriteHeader(wrapper.code)
|
||||||
|
|
||||||
|
if flusher, ok := wrapper.ResponseWriter.(http.Flusher); ok {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Package logger a package for handling writing content to logs.
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogLevel type definition of supported log levels.
|
||||||
|
type LogLevel int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Trace level of logs.
|
||||||
|
Trace LogLevel = iota - 2
|
||||||
|
// Debug level of logs.
|
||||||
|
Debug
|
||||||
|
// Info level of logs.
|
||||||
|
Info
|
||||||
|
// Warning level of logs.
|
||||||
|
Warning
|
||||||
|
// Error level of logs.
|
||||||
|
Error
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogWriter the struct used for writing logs.
|
||||||
|
type LogWriter struct {
|
||||||
|
level LogLevel
|
||||||
|
loggers map[LogLevel]*log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLogger create the LogWriter struct with required content.
|
||||||
|
func CreateLogger(level LogLevel) *LogWriter {
|
||||||
|
loggers := make(map[LogLevel]*log.Logger, Error-1)
|
||||||
|
|
||||||
|
loggers[Trace] = log.New(os.Stdout, "Rewrite-Body | TRACE", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
loggers[Debug] = log.New(os.Stdout, "Rewrite-Body | DEBUG", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
loggers[Info] = log.New(os.Stdout, "Rewrite-Body | INFO", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
loggers[Warning] = log.New(os.Stdout, "Rewrite-Body | WARNING", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
loggers[Error] = log.New(os.Stderr, "Rewrite-Body | ERROR", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
|
||||||
|
return &LogWriter{
|
||||||
|
level: level,
|
||||||
|
loggers: loggers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createLoggerWithBuffer(level LogLevel, buffer *bytes.Buffer) *LogWriter {
|
||||||
|
loggers := make(map[LogLevel]*log.Logger, Error-1)
|
||||||
|
|
||||||
|
loggers[Trace] = log.New(buffer, "Rewrite-Body | TRACE", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
loggers[Debug] = log.New(buffer, "Rewrite-Body | DEBUG", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
loggers[Info] = log.New(buffer, "Rewrite-Body | INFO", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
loggers[Warning] = log.New(buffer, "Rewrite-Body | WARNING", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
loggers[Error] = log.New(buffer, "Rewrite-Body | ERROR", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
|
|
||||||
|
return &LogWriter{
|
||||||
|
level: level,
|
||||||
|
loggers: loggers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *LogWriter) writeLog(level LogLevel, message string) {
|
||||||
|
if level < logger.level {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output := logger.loggers[level]
|
||||||
|
output.Print(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogTrace write Trace level logs.
|
||||||
|
func (logger *LogWriter) LogTrace(message string) {
|
||||||
|
logger.writeLog(Trace, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogDebug write Debug level logs.
|
||||||
|
func (logger *LogWriter) LogDebug(message string) {
|
||||||
|
logger.writeLog(Debug, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogInfo write Info level logs.
|
||||||
|
func (logger *LogWriter) LogInfo(message string) {
|
||||||
|
logger.writeLog(Info, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogWarning write Warning level logs.
|
||||||
|
func (logger *LogWriter) LogWarning(message string) {
|
||||||
|
logger.writeLog(Warning, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogError write Error level logs.
|
||||||
|
func (logger *LogWriter) LogError(message string) {
|
||||||
|
logger.writeLog(Error, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogTracef write Trace level logs with formatting similar to fmt.Sprintf.
|
||||||
|
func (logger *LogWriter) LogTracef(format string, a ...interface{}) {
|
||||||
|
logger.writeLog(Trace, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogDebugf write Debug level logs with formatting similar to fmt.Sprintf.
|
||||||
|
func (logger *LogWriter) LogDebugf(format string, a ...interface{}) {
|
||||||
|
logger.writeLog(Debug, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogInfof write Info level logs with formatting similar to fmt.Sprintf.
|
||||||
|
func (logger *LogWriter) LogInfof(format string, a ...interface{}) {
|
||||||
|
logger.writeLog(Info, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogWarningf write Warning level logs with formatting similar to fmt.Sprintf.
|
||||||
|
func (logger *LogWriter) LogWarningf(format string, a ...interface{}) {
|
||||||
|
logger.writeLog(Warning, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogErrorf write Error level logs with formatting similar to fmt.Sprintf.
|
||||||
|
func (logger *LogWriter) LogErrorf(format string, a ...interface{}) {
|
||||||
|
logger.writeLog(Error, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogging(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
configLevel LogLevel
|
||||||
|
targetLevel LogLevel
|
||||||
|
shouldWrite bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Trace level should write when targeting Trace",
|
||||||
|
configLevel: Trace,
|
||||||
|
targetLevel: Trace,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Trace level should write when targeting Debug",
|
||||||
|
configLevel: Trace,
|
||||||
|
targetLevel: Debug,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Trace level should write when targeting Info",
|
||||||
|
configLevel: Trace,
|
||||||
|
targetLevel: Info,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Trace level should write when targeting Warning",
|
||||||
|
configLevel: Trace,
|
||||||
|
targetLevel: Warning,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Trace level should write when targeting Error",
|
||||||
|
configLevel: Trace,
|
||||||
|
targetLevel: Error,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Debug level should NOT write when targeting Trace",
|
||||||
|
configLevel: Debug,
|
||||||
|
targetLevel: Trace,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Debug level should write when targeting Debug",
|
||||||
|
configLevel: Debug,
|
||||||
|
targetLevel: Debug,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Debug level should write when targeting Info",
|
||||||
|
configLevel: Debug,
|
||||||
|
targetLevel: Info,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Debug level should write when targeting Warning",
|
||||||
|
configLevel: Debug,
|
||||||
|
targetLevel: Warning,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Debug level should write when targeting Error",
|
||||||
|
configLevel: Debug,
|
||||||
|
targetLevel: Error,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Info level should NOT write when targeting Trace",
|
||||||
|
configLevel: Info,
|
||||||
|
targetLevel: Trace,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Info level should NOT write when targeting Debug",
|
||||||
|
configLevel: Info,
|
||||||
|
targetLevel: Debug,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Info level should write when targeting Info",
|
||||||
|
configLevel: Info,
|
||||||
|
targetLevel: Info,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Info level should write when targeting Warning",
|
||||||
|
configLevel: Info,
|
||||||
|
targetLevel: Warning,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Info level should write when targeting Error",
|
||||||
|
configLevel: Info,
|
||||||
|
targetLevel: Error,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Warning level should NOT write when targeting Trace",
|
||||||
|
configLevel: Warning,
|
||||||
|
targetLevel: Trace,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Warning level should NOT write when targeting Debug",
|
||||||
|
configLevel: Warning,
|
||||||
|
targetLevel: Debug,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Warning level should NOT write when targeting Info",
|
||||||
|
configLevel: Warning,
|
||||||
|
targetLevel: Info,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Warning level should write when targeting Warning",
|
||||||
|
configLevel: Warning,
|
||||||
|
targetLevel: Warning,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Warning level should write when targeting Error",
|
||||||
|
configLevel: Warning,
|
||||||
|
targetLevel: Error,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Warning level should NOT write when targeting Trace",
|
||||||
|
configLevel: Warning,
|
||||||
|
targetLevel: Trace,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Warning level should NOT write when targeting Debug",
|
||||||
|
configLevel: Warning,
|
||||||
|
targetLevel: Debug,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Warning level should NOT write when targeting Info",
|
||||||
|
configLevel: Warning,
|
||||||
|
targetLevel: Info,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Warning level should write when targeting Warning",
|
||||||
|
configLevel: Warning,
|
||||||
|
targetLevel: Warning,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Warning level should write when targeting Error",
|
||||||
|
configLevel: Warning,
|
||||||
|
targetLevel: Error,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Error level should NOT write when targeting Trace",
|
||||||
|
configLevel: Error,
|
||||||
|
targetLevel: Trace,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Error level should NOT write when targeting Debug",
|
||||||
|
configLevel: Error,
|
||||||
|
targetLevel: Debug,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Error level should NOT write when targeting Info",
|
||||||
|
configLevel: Error,
|
||||||
|
targetLevel: Info,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Error level should NOT write when targeting Warning",
|
||||||
|
configLevel: Error,
|
||||||
|
targetLevel: Warning,
|
||||||
|
shouldWrite: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Error level should write when targeting Error",
|
||||||
|
configLevel: Error,
|
||||||
|
targetLevel: Error,
|
||||||
|
shouldWrite: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
logger := createLoggerWithBuffer(test.configLevel, &buffer)
|
||||||
|
|
||||||
|
switch test.targetLevel {
|
||||||
|
case Trace:
|
||||||
|
logger.LogTrace("test")
|
||||||
|
case Debug:
|
||||||
|
logger.LogDebug("test")
|
||||||
|
case Info:
|
||||||
|
logger.LogInfo("test")
|
||||||
|
case Warning:
|
||||||
|
logger.LogWarning("test")
|
||||||
|
case Error:
|
||||||
|
logger.LogError("test")
|
||||||
|
}
|
||||||
|
|
||||||
|
loggedContent := buffer.Bytes()
|
||||||
|
if !test.shouldWrite {
|
||||||
|
if len(loggedContent) != 0 {
|
||||||
|
t.Errorf("Log level %v written with test value set to %v: '%s'",
|
||||||
|
test.targetLevel, test.configLevel, loggedContent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(loggedContent) == 0 {
|
||||||
|
t.Errorf("Log level %v NOT written with test value set to %v: '%s'",
|
||||||
|
test.targetLevel, test.configLevel, loggedContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Package rewrite_body a plugin to rewrite response body.
|
||||||
|
package rewrite_body
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/packruler/rewrite-body/handler"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateConfig creates and initializes the plugin configuration.
|
||||||
|
func CreateConfig() *handler.Config {
|
||||||
|
return &handler.Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates and returns a new rewrite body plugin instance.
|
||||||
|
func New(context context.Context, next http.Handler, config *handler.Config, name string) (http.Handler, error) {
|
||||||
|
return handler.New(context, next, config, name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package rewrite_body
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/packruler/rewrite-body/compressutil"
|
||||||
|
"github.com/packruler/rewrite-body/handler"
|
||||||
|
"github.com/packruler/rewrite-body/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServeHTTP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
contentEncoding string
|
||||||
|
contentType string `default:"text/html"`
|
||||||
|
rewrites []handler.Rewrite
|
||||||
|
lastModified bool
|
||||||
|
resBody string
|
||||||
|
expResBody string
|
||||||
|
expLastModified bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "should replace foo by bar",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentType: "text/html",
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "bar is the new bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should replace foo by bar, then by foo",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Regex: "bar",
|
||||||
|
Replacement: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentType: "text/html",
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "foo is the new foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should not replace anything if content encoding is not identity or empty",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "other",
|
||||||
|
contentType: "text/html",
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "foo is the new bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should not replace anything if content type does not contain text or is not empty",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentType: "image",
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "foo is the new bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should replace foo by bar if content encoding is identity",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "identity",
|
||||||
|
contentType: "text/html",
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "bar is the new bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should not remove the last modified header",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "identity",
|
||||||
|
contentType: "text/html",
|
||||||
|
lastModified: true,
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "bar is the new bar",
|
||||||
|
expLastModified: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should support gzip encoding",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "gzip",
|
||||||
|
contentType: "text/html",
|
||||||
|
lastModified: true,
|
||||||
|
resBody: compressString("foo is the new bar", "gzip"),
|
||||||
|
expResBody: compressString("bar is the new bar", "gzip"),
|
||||||
|
expLastModified: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should support deflate encoding",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "deflate",
|
||||||
|
contentType: "text/html",
|
||||||
|
lastModified: true,
|
||||||
|
resBody: compressString("foo is the new bar", "deflate"),
|
||||||
|
expResBody: compressString("bar is the new bar", "deflate"),
|
||||||
|
expLastModified: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should ignore unsupported encoding",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentEncoding: "br",
|
||||||
|
contentType: "text/html",
|
||||||
|
lastModified: true,
|
||||||
|
resBody: "foo is the new bar",
|
||||||
|
expResBody: "foo is the new bar",
|
||||||
|
expLastModified: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
config := &handler.Config{
|
||||||
|
LastModified: test.lastModified,
|
||||||
|
Rewrites: test.rewrites,
|
||||||
|
LogLevel: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
next := func(responseWriter http.ResponseWriter, req *http.Request) {
|
||||||
|
responseWriter.Header().Set("Content-Encoding", test.contentEncoding)
|
||||||
|
responseWriter.Header().Set("Content-Type", test.contentType)
|
||||||
|
responseWriter.Header().Set("Last-Modified", "Thu, 02 Jun 2016 06:01:08 GMT")
|
||||||
|
responseWriter.Header().Set("Content-Length", strconv.Itoa(len(test.resBody)))
|
||||||
|
responseWriter.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(responseWriter, test.resBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
rewriteBody, err := New(context.Background(), http.HandlerFunc(next), config, "rewriteBody")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Set("Accept", "text/html")
|
||||||
|
|
||||||
|
rewriteBody.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
|
if _, exists := recorder.Result().Header["Last-Modified"]; exists != test.expLastModified {
|
||||||
|
t.Errorf("got last-modified header %v, want %v", exists, test.expLastModified)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := recorder.Result().Header["Content-Length"]; exists {
|
||||||
|
t.Error("The Content-Length Header must be deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal([]byte(test.expResBody), recorder.Body.Bytes()) {
|
||||||
|
t.Errorf("got body: %v\n wanted: %v", recorder.Body.Bytes(), []byte(test.expResBody))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressString(value string, encoding string) string {
|
||||||
|
compressed, _ := compressutil.Encode([]byte(value), encoding)
|
||||||
|
|
||||||
|
return string(compressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
rewrites []handler.Rewrite
|
||||||
|
expErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "should return no error",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "foo",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Regex: "bar",
|
||||||
|
Replacement: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return an error",
|
||||||
|
rewrites: []handler.Rewrite{
|
||||||
|
{
|
||||||
|
Regex: "*",
|
||||||
|
Replacement: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMonitoring := httputil.MonitoringConfig{
|
||||||
|
Types: []string{"text/html"},
|
||||||
|
Methods: []string{http.MethodGet},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
config := &handler.Config{
|
||||||
|
Rewrites: test.rewrites,
|
||||||
|
Monitoring: defaultMonitoring,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := New(context.Background(), nil, config, "rewriteBody")
|
||||||
|
if test.expErr && err == nil {
|
||||||
|
t.Fatal("expected error on bad regexp format")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,10 @@ connector.name=iceberg
|
|||||||
iceberg.catalog.type=nessie
|
iceberg.catalog.type=nessie
|
||||||
iceberg.nessie-catalog.uri=http://nessie:19120/api/v2
|
iceberg.nessie-catalog.uri=http://nessie:19120/api/v2
|
||||||
iceberg.nessie-catalog.ref=main
|
iceberg.nessie-catalog.ref=main
|
||||||
iceberg.nessie-catalog.default-warehouse-dir=s3://lakehouse/
|
iceberg.nessie-catalog.default-warehouse-dir=${ENV:S3_WAREHOUSE}
|
||||||
fs.native-s3.enabled=true
|
fs.native-s3.enabled=true
|
||||||
s3.endpoint=http://rustfs:9000
|
s3.endpoint=${ENV:S3_ENDPOINT}
|
||||||
s3.path-style-access=true
|
s3.path-style-access=true
|
||||||
s3.region=us-east-1
|
s3.region=${ENV:S3_REGION}
|
||||||
s3.aws-access-key=${ENV:NESSIE_S3_ACCESS_KEY}
|
s3.aws-access-key=${ENV:NESSIE_S3_ACCESS_KEY}
|
||||||
s3.aws-secret-key=${ENV:NESSIE_S3_SECRET_KEY}
|
s3.aws-secret-key=${ENV:NESSIE_S3_SECRET_KEY}
|
||||||
|
|||||||
Reference in New Issue
Block a user