Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM mcr.microsoft.com/devcontainers/java:3-25-trixie@sha256:290d873937f3684458fc150a0ec968d6e54dc3d41c2374e3ea5a16c2689ce6d2

COPY certs/ /usr/local/share/ca-certificates/idp/

RUN update-ca-certificates
3 changes: 3 additions & 0 deletions .devcontainer/certs/.gitkeep
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
56 changes: 56 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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,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"
}
19 changes: 19 additions & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions .devcontainer/post-start.sh
Original file line number Diff line number Diff line change
@@ -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"
155 changes: 155 additions & 0 deletions .devcontainer/scripts/dev-services.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/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 <start|stop|restart|status|logs> [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"
)

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" ]
}

# Each service is launched under job control so it gets its own process
# group (pgid == its pid): signalling "-$pid" reaches the whole group.
running_pid() {
local pid
pid="$(cat "$IDP_STATE_DIR/$1.pid" 2>/dev/null)" || return 1
[ -n "$pid" ] && kill -0 "-$pid" 2>/dev/null && echo "$pid"
Comment on lines +40 to +41
}

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
(
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>&- &
echo "$!" >"$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") <start|stop|restart|status|logs> [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"
58 changes: 58 additions & 0 deletions .devcontainer/scripts/export-host-ca-certs.sh
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
* text=auto eol=lf
*.sh text eol=lf
mvnw text eol=lf
*.cmd text eol=crlf
*.bat text eol=crlf
15 changes: 14 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading