diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..11bee0a6 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/devcontainers/java:3-25-trixie@sha256:290d873937f3684458fc150a0ec968d6e54dc3d41c2374e3ea5a16c2689ce6d2 + +COPY certs/ /tmp/idp-certs/ + +# update-ca-certificates only imports "*.crt" files, so split every staged +# "*.crt"/"*.pem" file (including multi-certificate bundles) into one PEM +# certificate per "*.crt" file before running it. +RUN dir=/usr/local/share/ca-certificates/idp \ + && mkdir -p "$dir" \ + && i=0 \ + && for f in /tmp/idp-certs/*.crt /tmp/idp-certs/*.pem; do \ + [ -f "$f" ] || continue; \ + i=$((i + 1)); \ + awk -v prefix="$dir/idp-$i" ' \ + { sub(/\r$/, "") } \ + /-----BEGIN CERTIFICATE-----/ { n++; file = prefix "-" n ".crt" } \ + file { print > file } \ + /-----END CERTIFICATE-----/ { if (file) close(file); file = "" } \ + ' "$f"; \ + done \ + && rm -rf /tmp/idp-certs \ + && update-ca-certificates diff --git a/.devcontainer/certs/.gitkeep b/.devcontainer/certs/.gitkeep new file mode 100644 index 00000000..c5666aa7 --- /dev/null +++ b/.devcontainer/certs/.gitkeep @@ -0,0 +1,3 @@ +# Keeps the optional certificate directory in the Docker build context. +# Populated automatically per-host by scripts/export-host-ca-certs.sh +# (initializeCommand); contents are gitignored and never committed. diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 00000000..d02eddda --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,19 @@ +{ + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:4.1.1": { + "version": "4.1.1", + "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:2b44bcb32e75d5a3c028479f7f957cf7d5e42d81761373025a8d501dd3748f88", + "integrity": "sha256:2b44bcb32e75d5a3c028479f7f957cf7d5e42d81761373025a8d501dd3748f88" + }, + "ghcr.io/devcontainers/features/github-cli:1.1.2": { + "version": "1.1.2", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:7c409bf6316ffd85f04fd92fba85a744282639e603417dd4796409866bdb6805", + "integrity": "sha256:7c409bf6316ffd85f04fd92fba85a744282639e603417dd4796409866bdb6805" + }, + "ghcr.io/va-h/devcontainers-features/uv:1.1.4": { + "version": "1.1.4", + "resolved": "ghcr.io/va-h/devcontainers-features/uv@sha256:a15737142539d150ef4d358e2d6a7424a0a2f3dc43b29a3aa1b50162a8b11bc1", + "integrity": "sha256:a15737142539d150ef4d358e2d6a7424a0a2f3dc43b29a3aa1b50162a8b11bc1" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..020eb755 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,56 @@ +{ + "name": "Internal-Developer-Platform", + "initializeCommand": ["bash", ".devcontainer/scripts/export-host-ca-certs.sh"], + "build": { + "dockerfile": "Dockerfile", + "context": "." + }, + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:4.1.1": { + "moby": false, + "dockerDashComposeVersion": "v2" + }, + "ghcr.io/devcontainers/features/github-cli:1.1.2": {}, + "ghcr.io/va-h/devcontainers-features/uv:1.1.4": {} + }, + "remoteUser": "vscode", + "updateRemoteUserUID": true, + "containerEnv": { + "UV_NATIVE_TLS": "true" + }, + "hostRequirements": { + "cpus": 4, + "memory": "8gb" + }, + "mounts": [ + "source=idp-core-m2,target=/home/vscode/.m2,type=volume", + "source=idp-core-dind-${devcontainerId},target=/var/lib/docker,type=volume" + ], + "customizations": { + "vscode": { + "extensions": ["vscjava.vscode-java-pack"], + "settings": { + "java.autobuild.enabled": true + } + } + }, + "forwardPorts": [8084, 8000, 5437], + "portsAttributes": { + "8084": { + "label": "Spring Boot" + }, + "8000": { + "label": "Documentation", + "onAutoForward": "openPreview" + }, + "5437": { + "label": "PostgreSQL", + "onAutoForward": "silent" + } + }, + "otherPortsAttributes": { + "onAutoForward": "ignore" + }, + "postCreateCommand": "bash .devcontainer/post-create.sh", + "postStartCommand": "bash .devcontainer/post-start.sh" +} diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100644 index 00000000..db60cb1c --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +sudo apt-get update +sudo apt-get install --yes --no-install-recommends postgresql-client +sudo rm -rf /var/lib/apt/lists/* + +mkdir -p /home/vscode/.m2 +sudo chown -R vscode:vscode /home/vscode/.m2 + +uv sync --frozen --directory docs +uv tool install pre-commit +pre-commit install + +# Remove any stale target/ (e.g. from a prebuild on an older commit) so +# Maven's timestamp-based staleness check can't skip recompiling sources +# that were added/changed since it was generated. +./mvnw -B -ntp clean +./mvnw -B -ntp dependency:go-offline diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh new file mode 100644 index 00000000..cadf9e6b --- /dev/null +++ b/.devcontainer/post-start.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$(realpath "$0")")/.." +services=(bash .devcontainer/scripts/dev-services.sh) +result=0 + +"${services[@]}" start docs || result=1 + +wait_for() { + for attempt in $(seq 1 30); do + if "$@" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + return 1 +} + +if ! wait_for docker info; then + echo "Docker daemon did not become ready; app startup skipped" >&2 + result=1 +elif ! docker compose up -d postgres; then + echo "PostgreSQL container failed to start; app startup skipped" >&2 + result=1 +elif ! wait_for pg_isready -h localhost -p 5437 -U idpcore -d idpcore; then + echo "PostgreSQL did not become ready on localhost:5437; app startup skipped" >&2 + result=1 +else + "${services[@]}" start app || result=1 +fi + +"${services[@]}" status all || result=1 +exit "$result" diff --git a/.devcontainer/scripts/dev-services.sh b/.devcontainer/scripts/dev-services.sh new file mode 100644 index 00000000..e8d03272 --- /dev/null +++ b/.devcontainer/scripts/dev-services.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# Manages the "docs" and "app" dev services (not PostgreSQL, which is owned by +# docker-compose). Both already watch/reload themselves (zensical serve, +# Spring DevTools) so this only handles the one-time detached launch, health +# gating, and teardown. Usage: +# dev-services.sh [docs|app|all] +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root_dir="$(cd "$script_dir/../.." && pwd)" + +# Isolates state per checkout so multiple clones on the same host don't collide. +export IDP_STATE_DIR="/tmp/idp-dev-services-$(id -u)-$(printf '%s' "$root_dir" | sha256sum | cut -c1-12)" +mkdir -p -m 700 "$IDP_STATE_DIR" + +declare -A PORT=([docs]=8000 [app]=8084) +declare -A URL_PATH=([docs]=/ [app]=/actuator/health) +declare -A READY_TIMEOUT=([docs]=120 [app]=300) +declare -A COMMAND=( + [docs]="uv run --frozen --directory docs zensical serve --dev-addr 0.0.0.0:8000" + [app]="$root_dir/mvnw -B -ntp spring-boot:run -Dspring-boot.run.profiles=local,devcontainer,secret" +) + +port_occupied() { + (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null +} + +# Spring Boot's default actuator health mapping is 200 only when UP (503 otherwise). +is_healthy() { + local name="$1" url code + url="http://127.0.0.1:${PORT[$name]}${URL_PATH[$name]}" + code="$(curl --noproxy '*' -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)" + [ "$code" = "200" ] +} + +# Prints the start time (clock ticks since boot) of a process that is still +# its own process group leader. Fields are read after the last ")" because +# the command name in /proc//stat may contain spaces or parentheses. +leader_start_time() { + local stat fields + stat="$(cat "/proc/$1/stat" 2>/dev/null)" || return 1 + read -r -a fields <<<"${stat##*) }" + [ "${fields[2]:-}" = "$1" ] && [ -n "${fields[19]:-}" ] && echo "${fields[19]}" +} + +# Each service is launched under job control so it gets its own process +# group (pgid == its pid): signalling "-$pid" reaches the whole group. The PID +# file also stores the leader's start time so a reused PID/PGID left behind by +# an abrupt stop is never mistaken for (and signalled as) a managed service. +running_pid() { + local pid start + read -r pid start 2>/dev/null <"$IDP_STATE_DIR/$1.pid" || return 1 + [ -n "$pid" ] && [ -n "$start" ] || return 1 + [ "$(leader_start_time "$pid")" = "$start" ] || return 1 + kill -0 "-$pid" 2>/dev/null && echo "$pid" +} + +svc_status() { + local name="$1" log="$IDP_STATE_DIR/$name.log" + local url="http://127.0.0.1:${PORT[$name]}${URL_PATH[$name]}" + if running_pid "$name" >/dev/null; then + if is_healthy "$name"; then + echo "$name: ready, $url; log: $log" + return 0 + fi + echo "$name: starting or unhealthy; log: $log" + return 1 + fi + if port_occupied "${PORT[$name]}"; then + echo "$name: port ${PORT[$name]} is externally owned; left untouched" + else + echo "$name: stopped; log: $log" + fi + return 1 +} + +svc_start() { + local name="$1" deadline pid + ( + flock -x 200 + if running_pid "$name" >/dev/null || port_occupied "${PORT[$name]}"; then + exit 0 + fi + rm -f "$IDP_STATE_DIR/$name.pid" + # Job control (not setsid, which forks a copy under bash job control and + # would make $! point at the wrong, short-lived PID) puts this in its own + # process group so it doesn't share ours. Close the lock fd on the + # backgrounded command itself: it outlives this subshell and would + # otherwise keep the flock held forever through its inherited copy. + set -m + bash -c "exec ${COMMAND[$name]}" >"$IDP_STATE_DIR/$name.log" 2>&1 200>&- & + pid="$!" + echo "$pid $(leader_start_time "$pid" || true)" >"$IDP_STATE_DIR/$name.pid" + disown + ) 200>"$IDP_STATE_DIR/$name.lock" + + if ! running_pid "$name" >/dev/null; then + if port_occupied "${PORT[$name]}"; then + echo "$name: port ${PORT[$name]} already occupied; skipping managed launch" + return 0 + fi + echo "$name: failed to launch; log: $IDP_STATE_DIR/$name.log" >&2 + return 1 + fi + + deadline=$((SECONDS + READY_TIMEOUT[$name])) + while [ "$SECONDS" -lt "$deadline" ]; do + is_healthy "$name" && { svc_status "$name"; return $?; } + running_pid "$name" >/dev/null || break + sleep 0.25 + done + echo "$name: failed to become ready; log: $IDP_STATE_DIR/$name.log" >&2 + tail -n 30 "$IDP_STATE_DIR/$name.log" >&2 2>/dev/null || true + svc_stop "$name" >/dev/null 2>&1 || true + return 1 +} + +svc_stop() { + local name="$1" pid + if pid="$(running_pid "$name")" && [ -n "$pid" ]; then + kill -TERM "-$pid" 2>/dev/null || true + for _ in $(seq 1 50); do + kill -0 "-$pid" 2>/dev/null || break + sleep 0.1 + done + kill -0 "-$pid" 2>/dev/null && kill -KILL "-$pid" 2>/dev/null || true + echo "$name: stopped managed process" + fi + rm -f "$IDP_STATE_DIR/$name.pid" + if port_occupied "${PORT[$name]}"; then + echo "$name: port ${PORT[$name]} still occupied; left untouched" + return 1 + fi + return 0 +} + +svc_logs() { + local name="$1" log="$IDP_STATE_DIR/$name.log" + echo "$name: $log" + [ -f "$log" ] && tail -n 80 "$log" + return 0 +} + +usage() { + echo "Usage: $(basename "$0") [docs|app|all]" >&2 + exit 2 +} + +action="${1:-}" +target="${2:-all}" +case "$action" in start | stop | restart | status | logs) ;; *) usage ;; esac +case "$target" in docs | app | all) ;; *) usage ;; esac + +names=(docs app) +[ "$target" != "all" ] && names=("$target") + +status=0 +for name in "${names[@]}"; do + case "$action" in + start) svc_start "$name" || status=1 ;; + stop) svc_stop "$name" || status=1 ;; + restart) + svc_stop "$name" || true + svc_start "$name" || status=1 + ;; + status) svc_status "$name" || status=1 ;; + logs) svc_logs "$name" || status=1 ;; + esac +done +exit "$status" diff --git a/.devcontainer/scripts/export-host-ca-certs.sh b/.devcontainer/scripts/export-host-ca-certs.sh new file mode 100644 index 00000000..2eeccfbe --- /dev/null +++ b/.devcontainer/scripts/export-host-ca-certs.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Stages any corporate/proxy CA certificates trusted by the *host* machine into +# .devcontainer/certs/ so the container can trust the same TLS-inspecting +# proxy (Zscaler, Netskope, custom internal CA, ...) during the image build +# (docker-in-docker's github.com fetch, apt, etc.). +# +# Runs on the host via devcontainer.json's "initializeCommand", before every +# build. Never commits anything: .devcontainer/certs/* is gitignored, and this +# script wipes/repopulates it fresh on each run so it always reflects the +# current host, whoever/whatever machine that is. +set -uo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +certs_dir="$(cd "$script_dir/.." && pwd)/certs" + +mkdir -p "$certs_dir" +find "$certs_dir" -maxdepth 1 -type f ! -name '.gitkeep' -delete 2>/dev/null + +copy_source() { + local src="$1" + if [ -f "$src" ]; then + cp "$src" "$certs_dir/" 2>/dev/null + elif [ -d "$src" ]; then + find "$src" -maxdepth 2 -type f \( -name '*.crt' -o -name '*.pem' \) 2>/dev/null | + while IFS= read -r f; do cp "$f" "$certs_dir/" 2>/dev/null; done + fi +} + +# Explicit opt-in override: point at your own cert/bundle for anything the +# auto-detection below doesn't recognize. +if [ -n "${DEVCONTAINER_EXTRA_CA_CERTS:-}" ]; then + copy_source "$DEVCONTAINER_EXTRA_CA_CERTS" +fi + +# Best-effort auto-detection per host OS. Never fatal: an empty certs/ just +# means the build proceeds without extra trust, same as today. +case "$(uname -s 2>/dev/null)" in +Darwin) + # Dump the System keychain; duplicates of certs already in the image are harmless. + if command -v security >/dev/null 2>&1; then + security find-certificate -a -p /Library/Keychains/System.keychain 2>/dev/null | + awk -v dir="$certs_dir" ' + /-----BEGIN CERTIFICATE-----/ { n++; file = dir "/host-ca-" n ".pem" } + file { print > file } + ' + fi + ;; +Linux | MINGW* | MSYS* | CYGWIN*) + # Debian/Ubuntu and RHEL/Fedora both keep locally-added (i.e. corporate + # proxy installer) trust anchors separate from the default OS bundle here. + copy_source /usr/local/share/ca-certificates + copy_source /etc/pki/ca-trust/source/anchors + ;; +esac + +count=$(find "$certs_dir" -maxdepth 1 -type f ! -name '.gitkeep' 2>/dev/null | wc -l | tr -d ' ') +echo "[devcontainer] host CA export: ${count} certificate(s) staged in .devcontainer/certs (gitignored, build-context only)" +exit 0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..68462c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +*.sh text eol=lf +mvnw text eol=lf +*.cmd text eol=crlf +*.bat text eol=crlf diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bb7b73ab..fb437bc2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -22,9 +22,22 @@ updates: # Docker dependencies - package-ecosystem: "docker" + directories: + - "/" + - "/.devcontainer" + schedule: + interval: "weekly" + day: "monday" + time: "05:00" + open-pull-requests-limit: 5 + pull-request-branch-name: + separator: "-" + + - package-ecosystem: "devcontainers" directory: "/" schedule: - interval: "daily" + interval: "weekly" + day: "monday" time: "05:00" open-pull-requests-limit: 5 pull-request-branch-name: diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml new file mode 100644 index 00000000..f747b7a1 --- /dev/null +++ b/.github/workflows/devcontainer.yml @@ -0,0 +1,43 @@ +--- +name: Dev Container + +on: + pull_request: + paths: + - '.devcontainer/**' + - '.gitattributes' + - 'docker-compose.yml' + - '.github/workflows/devcontainer.yml' + push: + branches: + - main + paths: + - '.devcontainer/**' + - '.gitattributes' + - 'docker-compose.yml' + - '.github/workflows/devcontainer.yml' + workflow_dispatch: + +jobs: + verify: + permissions: + contents: read + name: Build and verify dev container + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Build and verify dev container + uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450 + with: + runCmd: | + set -euo pipefail + trap 'bash .devcontainer/scripts/dev-services.sh stop all' EXIT + bash .devcontainer/post-start.sh + before=$(bash .devcontainer/scripts/dev-services.sh status all) + bash .devcontainer/scripts/dev-services.sh start all + after=$(bash .devcontainer/scripts/dev-services.sh status all) + [[ "$before" == "$after" ]] + bash .devcontainer/scripts/dev-services.sh stop all + ./mvnw -B -ntp verify diff --git a/.gitignore b/.gitignore index 46a6e700..ab7589eb 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,10 @@ buildNumber.properties application-secret.* flyway-migration-order-report.md +# Local certificate authorities used by the dev container. +.devcontainer/certs/* +!.devcontainer/certs/.gitkeep + # BlueJ files *.ctxt diff --git a/docker-compose.yml b/docker-compose.yml index 13f77845..bbb95bd3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ services: postgres: - image: postgres:18 + image: postgres:18@sha256:86c951e05bf56c93d95d397747fb8820ac76cc3bedb78f43abd83eedbe3666ae environment: POSTGRES_USER: idpcore POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-idpcore_password} @@ -12,6 +12,11 @@ services: - ./scripts/init-extensions.sql:/docker-entrypoint-initdb.d/01-init-extensions.sql:ro networks: - postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready -U idpcore -d idpcore"] + interval: 5s + timeout: 5s + retries: 12 restart: unless-stopped networks: diff --git a/docs/src/concepts/authentication.md b/docs/src/concepts/authentication.md index 23d50ff2..7ef2d700 100644 --- a/docs/src/concepts/authentication.md +++ b/docs/src/concepts/authentication.md @@ -30,6 +30,13 @@ bypass authentication. > [!WARNING] > Never enable mock authentication in production. It skips all token validation. +The dev-container service script starts the app with the `local,devcontainer,secret` profiles, in that order. +Spring Boot gives later profiles precedence, so `devcontainer` disables JWT and OAuth2 login from `local`. +API routes therefore accept requests without credentials unless you enable another authentication mode. +The optional `secret` profile loads local overrides from `src/main/resources/application-secret.yml`; +Git ignores `application-secret.*` files. These overrides take precedence over `devcontainer`, so check them if +authentication remains enabled. The project-wide default keeps JWT authentication enabled. + ### JWT resource server The JWT chain validates `Authorization: Bearer ` requests as an OAuth2 resource server. It validates the diff --git a/docs/src/contributing/development-setup.md b/docs/src/contributing/development-setup.md index fdf2e9cb..f204a169 100644 --- a/docs/src/contributing/development-setup.md +++ b/docs/src/contributing/development-setup.md @@ -25,6 +25,147 @@ git --version pre-commit --version # If using pre-commit (recommended) ``` +## Development Container + +The repository includes a development container with Java 25, Maven, Docker, uv, +pre-commit, the GitHub command-line tool, and the PostgreSQL client. It starts the +PostgreSQL service, the documentation server, and Spring Boot automatically. + +### Supported Hosts + +| Host | Status | Requirement | +| ---- | ------ | ----------- | +| Debian with Docker Engine | Supported | Use a Docker daemon with root access | +| Windows with Docker Desktop | Supported | Keep the clone inside the WSL2 filesystem | +| WSL2 with Docker Engine | Supported | Use a Docker daemon with root access | +| macOS with Docker Desktop or OrbStack | Supported | Use a multi-architecture Docker runtime | +| Cloud development environment | Supported | Allocate at least 4 processors and 8 GB of memory | + +Docker rootless mode and other container management tools are not supported because the container uses +Docker-in-Docker for Testcontainers and the local Compose service. + +### Open the Container + +1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/) + on Windows or macOS, or install Docker Engine on Debian or WSL2. +2. Install [VS Code](https://code.visualstudio.com/) and the + [development container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). +3. Open the repository in VS Code. +4. Run **development containers: reopen in container**. + +The first build downloads the pinned tool images and dependencies. Later +rebuilds reuse named volumes for the Maven and Docker caches. +The container sets `UV_NATIVE_TLS=true` so uv trusts the operating system's +certificate authorities, including local certificates for inspecting proxies. + +After startup, open for the API and + for the documentation site. PostgreSQL is available on +port `5437` with the local credentials used by the `local` Spring profile. +Check the **Ports** panel for the actual forwarded addresses: VS Code can +choose another host port when the requested port is occupied. + +On Windows, clone the repository from a WSL2 terminal and open it from that +filesystem. Avoid `/mnt/c` because file sharing there significantly slows +Maven and file watching. + +### Watch and Reload + +Zensical runs with `serve`, which watches documentation sources and refreshes +the browser after changes. No separate watch command is required. + +Spring Boot starts with the `local,devcontainer,secret` profiles. Later +profiles override earlier ones. The optional `secret` profile loads a +Git-ignored `src/main/resources/application-secret.yml` file for local overrides. +The `devcontainer` profile disables JWT and OAuth2 login unless your local +overrides enable them. See +[Authentication](../concepts/authentication.md) for the resulting +authentication settings. + +Wait for VS Code to finish importing the Maven project, then save your Java +changes. Java auto-build compiles them into `target/classes`, and DevTools +restarts the app after successful compilation. Resolve compilation errors +in the **Problems** panel before expecting a reload. Resource changes also +trigger DevTools when the build copies them to the class path. + +This workflow depends on VS Code Java auto-build. For terminal-only editing, +run `./mvnw compile` after changes. The automatic launch does not use the +separate `dev` profile, which requires changes to `.reloadtrigger` for restarts. +Spring browser LiveReload is off for `local`; app restarts and +Zensical browser refresh remain enabled. + +### Service Controls + +Run these commands from the repository root: + +```bash +bash .devcontainer/scripts/dev-services.sh status all +bash .devcontainer/scripts/dev-services.sh logs app +bash .devcontainer/scripts/dev-services.sh stop app +bash .devcontainer/scripts/dev-services.sh start app +bash .devcontainer/scripts/dev-services.sh restart docs +``` + +Each command accepts `docs`, `app`, or `all`. The `all` option covers only docs +and Spring Boot; PostgreSQL remains under Docker Compose control. The `logs` +command prints recent output and the full log path. Use `tail -f` with that +path to follow output. Runtime state and logs live in a workspace-specific +directory under `/tmp`, outside the repository. + +Startup waits for HTTP readiness and reports failures with log output. The +first Spring Boot launch can take several minutes to compile. Docs startup +does not depend on database readiness. Repeated starts reuse managed services; +occupied ports belonging to other processes remain untouched. + +### Run Spring Boot Another Way + +Stop the managed app before launching it through VS Code **Run and Debug** or +your terminal: + +```bash +bash .devcontainer/scripts/dev-services.sh stop app +./mvnw spring-boot:run -Dspring-boot.run.profiles=local +``` + +Replace `local` with your required profile and supply its environment variables. +For VS Code, set `--spring.profiles.active=local` in your Java launch arguments, +or select your required profile instead. The container does not impose a +global Spring profile. + +The managed app stays stopped until you explicitly start it or restart the +container. Stop a manual launch with **Stop Debugging** or `Ctrl+C` before +running `start app` again. The helper never stops a manually launched process. + +### Troubleshoot Startup and Ports + +Use `status all` and `logs app` or `logs docs` to investigate startup failures. +For database failures, run `docker compose logs postgres`. Fix the reported +problem, then restart the affected service. The helper launches and manages each +service in its own process group. + +If a reload fails with unresolved generated mapper types after switching +branches or importing the project, stop the app, rebuild its generated +classes, then restart: + +```bash +bash .devcontainer/scripts/dev-services.sh stop app +./mvnw clean compile +bash .devcontainer/scripts/dev-services.sh start app +``` + +If IDE compilation errors persist, run **Java: clean Java Language Server +workspace** and wait for the Maven import to finish. + +Only ports `8000`, `8084`, and `5437` are automatically forwarded. Additional +listeners can belong to VS Code, Java tooling, or tests; a forwarded port does +not prove that an app is healthy. Run `ss -ltnp` to identify listeners +before stopping anything. Use **Stop Forwarding** to remove old entries from +the **Ports** panel, and forward debug ports manually when needed. + +After changing the container configuration, run **development containers: rebuild +container** to apply editor defaults and forwarding rules. Existing restored +forwarding entries might need removal once. Do not delete database volumes or +stop unrelated editor processes to clean up this list. + --- ## Clone Repository @@ -56,9 +197,10 @@ git remote add upstream https://github.com/Decathlon/internal-developer-platform ## Start the project -For your development setup, you can refer to the Getting started documentation here: [Getting Started](../getting-started/index.md) +For your development setup, you can refer to the getting started documentation here: [getting started](../getting-started/index.md) -Configure the application.yml security credentials and secrets locally using environment variables or a local, untracked configuration file. +Configure the `application.yml` security credentials and secrets locally using +environment variables or a local, untracked configuration file. ### Pre-Commit Hooks (Optional) diff --git a/mvnw.cmd b/mvnw.cmd index c453424c..8366e217 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,206 +1,206 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. >&2 -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. >&2 -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. >&2 -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. >&2 -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %WRAPPER_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file -SET WRAPPER_SHA_256_SUM="" -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B -) -IF NOT %WRAPPER_SHA_256_SUM%=="" ( - powershell -Command "&{"^ - "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ - "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ - "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ - " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ - " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ - " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ - " exit 1;"^ - "}"^ - "}" - if ERRORLEVEL 1 goto error -) - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. >&2 +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. >&2 +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/src/main/resources/application-devcontainer.yml b/src/main/resources/application-devcontainer.yml new file mode 100644 index 00000000..5445e3e2 --- /dev/null +++ b/src/main/resources/application-devcontainer.yml @@ -0,0 +1,7 @@ +app: + security: + authentication: + jwt: + enabled: false + oauth2-login: + enabled: false diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 50e9b598..8fab34bf 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -2,6 +2,9 @@ server: port: 8084 spring: + devtools: + livereload: + enabled: false datasource: url: jdbc:postgresql://localhost:5437/idpcore username: idpcore