From 8f4af5e3223d50d8f082a1b3f1c2a30d3687ead7 Mon Sep 17 00:00:00 2001 From: hitanshu310 Date: Tue, 7 Jul 2026 02:51:58 +0530 Subject: [PATCH] Hithomelabs/CFTunnels#99: Deploy portainer-automation service - Add Dockerfile for portainer-automation (multi-stage build) - Add standalone docker-compose.yaml for PA service - Add application-prod.properties (prod Portainer :9443) - Add application-ci.properties (CI test endpoints) - Add .env.example with documented env vars for PA - Add PORTAINER_STACK.md with deployment guide, env var tables, architecture diagram - Add CI workflow (portainer_automation_build_push.yml) for image build/push - Update root docker-compose.yaml to include PA service --- .../portainer_automation_build_push.yml | 66 ++++++ docker-compose.yaml | 14 +- portainer-automation/.env.example | 38 +++ portainer-automation/Dockerfile | 12 + portainer-automation/PORTAINER_STACK.md | 224 ++++++++++++++++++ portainer-automation/docker-compose.yaml | 13 + .../main/resources/application-ci.properties | 6 + .../resources/application-prod.properties | 6 + 8 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/portainer_automation_build_push.yml create mode 100644 portainer-automation/.env.example create mode 100644 portainer-automation/Dockerfile create mode 100644 portainer-automation/PORTAINER_STACK.md create mode 100644 portainer-automation/docker-compose.yaml create mode 100644 portainer-automation/src/main/resources/application-ci.properties create mode 100644 portainer-automation/src/main/resources/application-prod.properties diff --git a/.gitea/workflows/portainer_automation_build_push.yml b/.gitea/workflows/portainer_automation_build_push.yml new file mode 100644 index 0000000..0a68187 --- /dev/null +++ b/.gitea/workflows/portainer_automation_build_push.yml @@ -0,0 +1,66 @@ +name: portainer-automation build and push +run-name: PA Build started by ${{ gitea.actor }} +on: + push: + branches: [test] +jobs: + tag: + runs-on: ubuntu-latest + outputs: + new_version: ${{ steps.new_version.outputs.new_version }} + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get new version + id: new_version + run: | + VERSION=$(git describe --tags --abbrev=0) + echo "Current version: ${VERSION}" + MAJOR=$(echo ${VERSION} | cut -d "." -f 1) + MINOR=$(echo ${VERSION} | cut -d "." -f 2) + PATCH=$(echo ${VERSION} | cut -d "." -f 3) + NEW_PATCH=$((PATCH + 1)) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + echo "New version: ${NEW_VERSION}" + echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT + build_tag_push: + runs-on: ubuntu-latest + needs: tag + container: + image: 192.168.0.100:8928/hithomelabs/ci-runner:1.0.0 + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install JDK (Alpine package) + run: | + apk add --no-cache openjdk17-jdk + echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk" >> "$GITHUB_ENV" + - name: Validate Gradle Wrapper (offline checksum) + run: | + sha256sum --check gradle/wrapper/gradle-wrapper.jar.sha256 + - name: Create and push tag + run: | + echo "New version: ${{ needs.tag.outputs.new_version }}" + git config --global user.name "${{ gitea.actor }}" + git config --global user.email "${{ gitea.actor }}@users.noreply.github.com" + git tag -a "pa-${{ needs.tag.outputs.new_version }}" -m "Pushing PA version ${{ needs.tag.outputs.new_version }}" + git push origin "pa-${{ needs.tag.outputs.new_version }}" + - name: Log in to Gitea Docker Registry + uses: docker/login-action@v3 + with: + registry: 'http://192.168.0.100:8928' + username: hitanshu + password: ${{ secrets.TOKEN }} + - name: Gradle build image + run: ./gradlew :portainer-automation:bootBuildImage --imageName=192.168.0.100:8928/hithomelabs/portainer-automation:${{ needs.tag.outputs.new_version }} + - name: Tag image as test + run: docker tag 192.168.0.100:8928/hithomelabs/portainer-automation:${{ needs.tag.outputs.new_version }} 192.168.0.100:8928/hithomelabs/portainer-automation:test + - name: Push to Gitea Registry + run: | + docker push 192.168.0.100:8928/hithomelabs/portainer-automation:test + docker push 192.168.0.100:8928/hithomelabs/portainer-automation:${{ needs.tag.outputs.new_version }} diff --git a/docker-compose.yaml b/docker-compose.yaml index 1983e88..07f9090 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -29,4 +29,16 @@ services: ports: - "${DB_PORT}:5432" volumes: - - ${DB_PATH}:/var/lib/postgresql/data \ No newline at end of file + - ${DB_PATH}:/var/lib/postgresql/data + portainer-automation: + image: gitea.hithomelabs.com/hithomelabs/portainer-automation:${ENV} + container_name: portainer-automation_${ENV} + ports: + - "${PORTAINER_AUTOMATION_PORT:-8081}:8081" + environment: + - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-local} + - PORTAINER_API_KEY=${PORTAINER_API_KEY} + - PORTAINER_SERVICE_API_KEY=${PORTAINER_SERVICE_API_KEY} + env_file: + - stack.env + restart: unless-stopped \ No newline at end of file diff --git a/portainer-automation/.env.example b/portainer-automation/.env.example new file mode 100644 index 0000000..1d6593c --- /dev/null +++ b/portainer-automation/.env.example @@ -0,0 +1,38 @@ +# ============================================ +# Portainer Automation Service — Environment Variables +# ============================================ +# Copy this file to .env and fill in your values. +# This service is NOT exposed via Cloudflare Tunnel (internal-only). + +# --- Portainer Connection --- + +# Portainer API Key for the target Portainer instance +# Create via Portainer UI: Settings → Authentication → Access tokens +# Dev: https://devdocker.hithomelabs.com → ptk_xxxx_dev_xxxx +# Prod: https://192.168.0.100:9443 → ptk_xxxx_prod_xxxx +PORTAINER_API_KEY=ptr_your_portainer_api_key_here + +# Portainer endpoint ID (1 = primary Docker endpoint, 2 = remote) +PORTAINER_ENDPOINT_ID=2 + +# --- Service Auth --- + +# API key that CI workflows send in the X-API-Key header +# to authenticate against this service's /api/deploy endpoint +PORTAINER_SERVICE_API_KEY=change-me + +# --- Spring Profile --- + +# Active Spring profile (local | ci | prod) +# local — uses devdocker.hithomelabs.com via Cloudflare Tunnel (default) +# ci — uses test/stub endpoints +# prod — uses production Portainer on internal network +SPRING_PROFILES_ACTIVE=local + +# --- Deployment --- + +# Deployment environment label (dev | test | prod) +ENV=dev + +# Host port to map to container port 8081 +HOST_PORT=8081 diff --git a/portainer-automation/Dockerfile b/portainer-automation/Dockerfile new file mode 100644 index 0000000..93fb0b1 --- /dev/null +++ b/portainer-automation/Dockerfile @@ -0,0 +1,12 @@ +FROM openjdk:17-jdk as build +WORKDIR /app +COPY gradlew settings.gradle build.gradle ./ +COPY gradle ./gradle +COPY common ./common +COPY cftunnels-service ./cftunnels-service +COPY portainer-automation ./portainer-automation +RUN ./gradlew :portainer-automation:bootJar + +FROM openjdk:17-jdk-slim +COPY --from=build /app/portainer-automation/build/libs/*.jar app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/portainer-automation/PORTAINER_STACK.md b/portainer-automation/PORTAINER_STACK.md new file mode 100644 index 0000000..95a2aba --- /dev/null +++ b/portainer-automation/PORTAINER_STACK.md @@ -0,0 +1,224 @@ +# Portainer Automation Service — Portainer Stack Guide + +## Overview + +The Portainer Automation service exposes a single endpoint (`POST /api/deploy/{stackId}`) +that triggers a redeploy of a Portainer stack. CI workflows call this endpoint after +pushing new container images. + +The service is deployed as a **separate Portainer stack** (Option B) — independent from +the CFTunnels stacks — so it can be reused by other projects. + +--- + +## Architecture Diagram + +``` +┌─────────────────┐ push tag ┌──────────────────┐ +│ Gitea Runner │ ──────────────► │ Docker Registry │ +│ (CI workflow) │ │ :8928 │ +└────────┬────────┘ └──────────────────┘ + │ + │ POST /api/deploy/{stackId} + │ X-API-Key: + ▼ +┌─────────────────────────────────────┐ +│ Portainer Automation Service │ +│ (portainer-automation_${ENV}) │ +│ port: 8081 │ +└──────────┬──────────────────────────┘ + │ PUT /api/stacks/{id}/git/redeploy + │ X-API-Key: + ▼ +┌─────────────────────────────────────┐ +│ Portainer CE │ +│ Dev: :9442 / Prod: :9443 │ +└──────────┬──────────────────────────┘ + │ git pull + stack redeploy + ▼ +┌─────────────────────────────────────┐ +│ Target Stack (e.g. CFTunnels) │ +│ Docker Compose from Git │ +└─────────────────────────────────────┘ +``` + +--- + +## Portainer Stack Configuration + +### Create a New Stack in Portainer + +1. **Navigate**: Portainer UI → Stacks → **Add stack** +2. **Name**: `portainer-automation-{env}` (e.g. `portainer-automation-dev`) +3. **Build method**: **Git repository** +4. **Repository URL**: `https://gitea.hithomelabs.com/Hithomelabs/CFTunnels.git` +5. **Repository reference**: `refs/heads/test` (dev) or `refs/heads/main` (prod) +6. **Compose path**: `portainer-automation/docker-compose.yaml` +7. **Environment variables**: See table below + +### Environment Variables — Dev + +| Variable | Value | Description | +|----------|-------|-------------| +| `ENV` | `dev` | Environment label | +| `HOST_PORT` | `8081` | Host port mapping | +| `SPRING_PROFILES_ACTIVE` | `local` | Spring profile (uses `devdocker.hithomelabs.com`) | +| `PORTAINER_API_KEY` | `ptk_xxxx_dev_xxxx` | Portainer Dev API access token | +| `PORTAINER_SERVICE_API_KEY` | *(generate a random key)* | CI → Service auth key | + +Portainer Dev endpoint: `https://devdocker.hithomelabs.com` (port 9442 behind Cloudflare Tunnel) + +### Environment Variables — Prod + +| Variable | Value | Description | +|----------|-------|-------------| +| `ENV` | `prod` | Environment label | +| `HOST_PORT` | `8082` | Host port mapping | +| `SPRING_PROFILES_ACTIVE` | `prod` | Spring profile (uses `192.168.0.100:9443`) | +| `PORTAINER_API_KEY` | `ptk_xxxx_prod_xxxx` | Portainer Prod API access token | +| `PORTAINER_SERVICE_API_KEY` | *(generate a different random key)* | CI → Service auth key | + +Portainer Prod endpoint: `https://192.168.0.100:9443` (internal network) + +--- + +## Deployment Steps + +### Prerequisites + +1. Portainer Automation Docker image is built and pushed (see CI workflow) +2. Portainer API access tokens created (Portainer UI → My access tokens) +3. Gitea secrets configured: + - `PORTAINER_SERVICE_API_KEY` — shared secret for dev + - `PORTAINER_SERVICE_API_KEY_PROD` — shared secret for prod + +### Step 1: Build and Push Image + +The CI workflow `.gitea/workflows/portainer_automation_build_push.yml` handles this +automatically on push to the `test` branch. + +To build manually: +```bash +./gradlew :portainer-automation:bootBuildImage \ + --imageName=192.168.0.100:8928/hithomelabs/portainer-automation: +``` + +### Step 2: Create Stack in Portainer (Dev) + +1. Log in to Portainer Dev (`https://devdocker.hithomelabs.com`) +2. Stacks → Add stack +3. Name: `portainer-automation-dev` +4. Build method: Git repository +5. Repository: `https://gitea.hithomelabs.com/Hithomelabs/CFTunnels.git` +6. Reference: `refs/heads/test` +7. Compose path: `portainer-automation/docker-compose.yaml` +8. Add env vars from the **Dev** table above +9. Click **Deploy the stack** + +### Step 3: Verify Dev Deployment + +```bash +# Redeploy a stack via the automation service +curl -X POST \ + -H "X-API-Key: " \ + http://portainer-automation-dev:8081/api/deploy/ + +# Expected: "Deployment initiated for stack " +``` + +### Step 4: Create Stack in Portainer (Prod) + +1. Log in to Portainer Prod (`https://192.168.0.100:9443`) +2. Follow the same steps as Step 2, with: + - Name: `portainer-automation-prod` + - Reference: `refs/heads/main` + - Env vars from the **Prod** table above + +### Step 5: Wire CI Triggers + +Ensure CI workflows call the automation endpoint: + +**Dev** (in `test_image_build_push.yml`): +```yaml +- name: Trigger Portainer Redeploy (Dev) + if: success() + env: + PORTAINER_AUTOMATION_URL: "http://portainer-automation-dev:8081" + STACK_ID: ${{ vars.PORTAINER_AUTOMATION_DEV_STACK_ID }} + API_KEY: ${{ secrets.PORTAINER_SERVICE_API_KEY }} + run: | + wget -q --no-check-certificate --timeout=30 \ + --header="X-API-Key: $API_KEY" \ + -O - \ + "$PORTAINER_AUTOMATION_URL/api/deploy/$STACK_ID" 2>&1 \ + || echo "Deploy trigger failed (non-fatal)" +``` + +**Prod** (in `prod_image_tag_promote.yaml`): +```yaml +- name: Trigger Portainer Redeploy (Prod) + if: success() + env: + PORTAINER_AUTOMATION_URL: "http://portainer-automation-prod:8082" + STACK_ID: ${{ vars.PORTAINER_AUTOMATION_PROD_STACK_ID }} + API_KEY: ${{ secrets.PORTAINER_SERVICE_API_KEY_PROD }} + run: | + wget -q --no-check-certificate --timeout=30 \ + --header="X-API-Key: $API_KEY" \ + -O - \ + "$PORTAINER_AUTOMATION_URL/api/deploy/$STACK_ID" 2>&1 \ + || echo "Deploy trigger failed (non-fatal)" +``` + +--- + +## Configuration Reference + +### Profile Resolution Order + +The service resolves configuration in this order (later overrides earlier): + +1. `application.properties` — default: `https://192.168.0.100:9442` +2. `application-{profile}.properties` — profile-specific overrides +3. Environment variables — highest priority + +### Profile Purposes + +| Profile | File | Portainer Target | Use Case | +|---------|------|-----------------|----------| +| default | `application.properties` | `192.168.0.100:9442` (Dev) | CI/CD, Docker compose | +| `local` | `application-local.properties` | `devdocker.hithomelabs.com` | Local dev via Cloudflare Tunnel | +| `ci` | `application-ci.properties` | `portainer-test:9000` | CI pipeline testing | +| `prod` | `application-prod.properties` | `192.168.0.100:9443` (Prod) | Production deployment | + +### Environment Variable Reference + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `SPRING_PROFILES_ACTIVE` | No | *(none)* | Active Spring profile | +| `PORTAINER_API_KEY` | Yes | — | Portainer API access token | +| `PORTAINER_SERVICE_API_KEY` | Yes | — | Shared secret for CI → service auth | +| `ENV` | No | `test` | Deployment environment label | +| `HOST_PORT` | No | `8081` | External port mapping | + +--- + +## Health Checks + +The service exposes no dedicated health endpoint yet. Verify availability by: + +```bash +# Check that the service responds (expects 401 Unauthorized without API key) +curl -X POST http://portainer-automation-dev:8081/api/deploy/1 +# Expected: {"timestamp":"...","status":401,"error":"Unauthorized",...} +# A 401 response means the service is running. +``` + +## Troubleshooting + +| Symptom | Likely Cause | Fix | +|---------|-------------|-----| +| `Connection refused` | Service not running | Check stack status in Portainer | +| `Invalid API key` | Wrong `PORTAINER_SERVICE_API_KEY` | Verify env var matches CI secret | +| `Portainer error: Unauthorized` | Wrong `PORTAINER_API_KEY` | Regenerate token in Portainer UI | +| `Stack not found` | Wrong stack ID | Verify stack ID in Portainer URL | diff --git a/portainer-automation/docker-compose.yaml b/portainer-automation/docker-compose.yaml new file mode 100644 index 0000000..3dad479 --- /dev/null +++ b/portainer-automation/docker-compose.yaml @@ -0,0 +1,13 @@ +services: + portainer-automation: + image: gitea.hithomelabs.com/hithomelabs/portainer-automation:${ENV:-test} + container_name: portainer-automation_${ENV:-test} + ports: + - "${HOST_PORT:-8081}:8081" + environment: + - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-local} + - PORTAINER_API_KEY=${PORTAINER_API_KEY} + - PORTAINER_SERVICE_API_KEY=${PORTAINER_SERVICE_API_KEY} + env_file: + - stack.env + restart: unless-stopped diff --git a/portainer-automation/src/main/resources/application-ci.properties b/portainer-automation/src/main/resources/application-ci.properties new file mode 100644 index 0000000..e6f56bc --- /dev/null +++ b/portainer-automation/src/main/resources/application-ci.properties @@ -0,0 +1,6 @@ +# CI-specific Portainer Configuration +# Uses test/stub endpoints during CI pipeline testing +portainer.base-url=http://portainer-test:9000 +portainer.api-key=${PORTAINER_API_KEY:ci-test-key} +portainer.endpoint-id=1 +portainer.service.api-key=${PORTAINER_SERVICE_API_KEY:ci-test-key} diff --git a/portainer-automation/src/main/resources/application-prod.properties b/portainer-automation/src/main/resources/application-prod.properties new file mode 100644 index 0000000..54b367b --- /dev/null +++ b/portainer-automation/src/main/resources/application-prod.properties @@ -0,0 +1,6 @@ +# Production Portainer Configuration +# Targets the production Portainer instance on the internal network (port 9443) +portainer.base-url=https://192.168.0.100:9443 +portainer.api-key=${PORTAINER_API_KEY} +portainer.endpoint-id=2 +portainer.service.api-key=${PORTAINER_SERVICE_API_KEY}