commit 2dbc92c1a606460e6527d82ffb4bf84c3b884232 Author: Scaffolder Date: Wed Mar 18 22:24:36 2026 +0000 initial commit Change-Id: I11e12d8d4c914eabdc30ba4d8be494fd09b617a3 diff --git a/.gitea/workflows/build-push.yml b/.gitea/workflows/build-push.yml new file mode 100644 index 0000000..a879c81 --- /dev/null +++ b/.gitea/workflows/build-push.yml @@ -0,0 +1,104 @@ +name: Build and Push to ACR + +on: + push: + branches: [ "main" ] + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + +jobs: + build: + name: Build and Push + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: 'maven' + + - name: Install Maven 3.9 + run: | + MVN_VERSION=3.9.9 + wget -q "https://archive.apache.org/dist/maven/maven-3/${MVN_VERSION}/binaries/apache-maven-${MVN_VERSION}-bin.tar.gz" + tar -xzf "apache-maven-${MVN_VERSION}-bin.tar.gz" -C /opt + ln -sf "/opt/apache-maven-${MVN_VERSION}/bin/mvn" /usr/local/bin/mvn + mvn --version + + - name: Build with Maven + run: | + mvn clean package -DskipTests -B + echo "✓ Build complete" + + - name: Run tests + run: mvn test -B + + - name: Install Azure CLI + run: | + command -v az &>/dev/null || curl -sL https://aka.ms/InstallAzureCLIDeb | bash + + - name: Install Docker CLI + run: | + command -v docker &>/dev/null || (apt-get update -qq && apt-get install -y docker.io) + docker --version + + - name: Azure login (OIDC) + run: | + az login \ + --service-principal \ + --username "$AZURE_CLIENT_ID" \ + --tenant "$AZURE_TENANT_ID" \ + --federated-token "$(cat $AZURE_FEDERATED_TOKEN_FILE)" + echo "✓ Azure login successful" + + - name: Get ACR details + id: acr + run: | + ACR_NAME=$(az acr list --query "[0].name" -o tsv) + ACR_NAME="${ACR_NAME:-bstagecjotdevacr}" + echo "ACR_NAME=$ACR_NAME" >> $GITHUB_ENV + echo "ACR_LOGIN_SERVER=${ACR_NAME}.azurecr.io" >> $GITHUB_ENV + echo "✓ Using ACR: ${ACR_NAME}.azurecr.io" + + - name: ACR Login + run: | + ACR_TOKEN=$(az acr login --name "$ACR_NAME" --expose-token --output tsv --query accessToken) + docker login "$ACR_LOGIN_SERVER" \ + --username 00000000-0000-0000-0000-000000000000 \ + --password "$ACR_TOKEN" + echo "✓ ACR login successful" + + - name: Build and Push Docker image + run: | + IMAGE_TAG="${{ gitea.sha }}" + IMAGE_FULL="${ACR_LOGIN_SERVER}/test-alex-2:${IMAGE_TAG}" + IMAGE_LATEST="${ACR_LOGIN_SERVER}/test-alex-2:latest" + docker build -t "$IMAGE_FULL" -t "$IMAGE_LATEST" . + docker push "$IMAGE_FULL" + docker push "$IMAGE_LATEST" + echo "IMAGE_FULL=$IMAGE_FULL" >> $GITHUB_ENV + echo "✓ Pushed: $IMAGE_FULL" + + - name: Build Summary + run: | + echo "### ✅ Build Successful" >> $GITHUB_STEP_SUMMARY + echo "| | |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| **Service** | test-alex-2 |" >> $GITHUB_STEP_SUMMARY + echo "| **Runtime** | Java Spring Boot |" >> $GITHUB_STEP_SUMMARY + echo "| **Commit** | ${{ gitea.sha }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Image** | $IMAGE_FULL |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/deploy-humanitec.yml b/.gitea/workflows/deploy-humanitec.yml new file mode 100644 index 0000000..c1ed3a6 --- /dev/null +++ b/.gitea/workflows/deploy-humanitec.yml @@ -0,0 +1,188 @@ +name: Deploy to Humanitec + +on: + workflow_run: + workflows: ["Build and Push to ACR"] + types: + - completed + branches: [ "main" ] + workflow_dispatch: + inputs: + environment: + description: 'Target environment' + required: true + default: 'dev' + type: choice + options: + - dev + - staging + - prod + +env: + APP_ID: test-alex-2 + PROJECT_ID: test-alex-2 + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + DEFAULT_ENV_ID: dev + +jobs: + deploy: + name: Deploy to Humanitec + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' + permissions: + contents: read + id-token: write + outputs: + sha: ${{ steps.get-sha.outputs.sha }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get short SHA + id: get-sha + run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + + - name: Install CLI tools + run: | + # jq (needed for github API call) + command -v jq &>/dev/null || (apt-get update -qq && apt-get install -y -qq jq) + # hctl (Humanitec Platform Orchestrator v2 CLI) + HCTL_VERSION=$(curl -s https://api.github.com/repos/humanitec/hctl/releases/latest | jq -r '.tag_name') + curl -fLO "https://github.com/humanitec/hctl/releases/download/${HCTL_VERSION}/hctl_${HCTL_VERSION#v}_linux_amd64.tar.gz" + tar -xzf "hctl_${HCTL_VERSION#v}_linux_amd64.tar.gz" + install -m 755 hctl /usr/local/bin/hctl + hctl --version + echo "✓ Tools installed" + + - name: Install Azure CLI + run: command -v az &>/dev/null || curl -sL https://aka.ms/InstallAzureCLIDeb | bash + + - name: Azure login (OIDC) + run: | + az login \ + --service-principal \ + --username "$AZURE_CLIENT_ID" \ + --tenant "$AZURE_TENANT_ID" \ + --federated-token "$(cat $AZURE_FEDERATED_TOKEN_FILE)" + echo "✓ Azure login successful" + + - name: Get image reference + id: image + run: | + ACR_LOGIN_SERVER=$(az acr list --query "[0].loginServer" -o tsv) + SHA=${{ steps.get-sha.outputs.sha }} + echo "IMAGE_FULL=${ACR_LOGIN_SERVER}/test-alex-2:${SHA}" >> $GITHUB_ENV + echo "✓ Image: ${ACR_LOGIN_SERVER}/test-alex-2:${SHA}" + + - name: Set environment + id: set-env + run: | + if [ "${{ gitea.event_name }}" = "workflow_dispatch" ]; then + ENV_ID="${{ gitea.event.inputs.environment }}" + else + ENV_ID="$DEFAULT_ENV_ID" + fi + ENV_ID="${ENV_ID:-dev}" + echo "ENV_ID=$ENV_ID" >> $GITHUB_OUTPUT + echo "ENV_ID=$ENV_ID" >> $GITHUB_ENV + echo "✓ Target environment: $ENV_ID" + + - name: Get Humanitec credentials from Key Vault + run: | + HUMANITEC_TOKEN=$(az keyvault secret show \ + --vault-name "bstage-cjot-dev-core-kv" \ + --name "humanitec-api-token-v2" \ + --query "value" -o tsv) + [ -z "$HUMANITEC_TOKEN" ] && echo "❌ Failed to retrieve HUMANITEC_TOKEN" && exit 1 + echo "HUMANITEC_AUTH_TOKEN=$HUMANITEC_TOKEN" >> $GITHUB_ENV + + HUMANITEC_ORG=$(az keyvault secret show \ + --vault-name "bstage-cjot-dev-core-kv" \ + --name "humanitec-org-id" \ + --query "value" -o tsv) + [ -z "$HUMANITEC_ORG" ] && echo "❌ Failed to retrieve HUMANITEC_ORG" && exit 1 + echo "HUMANITEC_ORG=$HUMANITEC_ORG" >> $GITHUB_ENV + echo "✓ Credentials retrieved" + + - name: Deploy via Humanitec Score + run: | + # Pre-flight: wait for any prior deployment to finish before calling hctl. + # hctl refuses to start a new deployment while one is still executing. + echo "Pre-flight: checking for in-progress deployments..." + PREFLIGHT_WAITED=0 + while [ $PREFLIGHT_WAITED -lt 300 ]; do + PREFLIGHT_STATUS=$(curl -sf \ + -H "Authorization: Bearer $HUMANITEC_AUTH_TOKEN" \ + "https://api.humanitec.dev/orgs/$HUMANITEC_ORG/last-deployments?env_id=$ENV_ID&project_id=$PROJECT_ID&state_change_only=true" \ + | jq -r '.items[0].status // "none"' 2>/dev/null || echo "none") + if [ "$PREFLIGHT_STATUS" != "in progress" ] && [ "$PREFLIGHT_STATUS" != "pending" ] && [ "$PREFLIGHT_STATUS" != "executing" ]; then + echo "Pre-flight passed (status=$PREFLIGHT_STATUS). Proceeding." + break + fi + echo " Prior deployment still running ($PREFLIGHT_WAITED s elapsed, status=$PREFLIGHT_STATUS)..." + sleep 15 + PREFLIGHT_WAITED=$((PREFLIGHT_WAITED + 15)) + done + # First deploy — provisions all resources. On a brand-new Humanitec project the + # dns-k8s-ingress Terraform module runs before the K8s Service exists, so the + # ingress backend port falls back to 3000. A second deploy (below) corrects it + # once the Service is up, which is essential for apps not running on port 3000. + HCTL_EXIT=0 + timeout 300 hctl score deploy "$PROJECT_ID" "$ENV_ID" score.yaml \ + --no-prompt \ + --default-image "$IMAGE_FULL" || HCTL_EXIT=$? + if [ "$HCTL_EXIT" -eq 0 ]; then + echo "✓ First deployment complete to $ENV_ID" + elif [ "$HCTL_EXIT" -eq 124 ]; then + echo "✓ First deployment submitted (polling timed out — waiting for K8s to settle)" + else + echo "✗ hctl failed with exit code $HCTL_EXIT" + exit $HCTL_EXIT + fi + # Poll Humanitec API until the first deployment is no longer in-progress before + # re-deploying. A flat sleep is unreliable — Terraform DNS modules can take 4-6 min. + echo "Waiting for first deployment to finish (polling Humanitec API)..." + MAX_WAIT=360 + WAITED=0 + while [ $WAITED -lt $MAX_WAIT ]; do + DEPLOY_STATUS=$(curl -sf \ + -H "Authorization: Bearer $HUMANITEC_AUTH_TOKEN" \ + "https://api.humanitec.dev/orgs/$HUMANITEC_ORG/last-deployments?env_id=$ENV_ID&project_id=$PROJECT_ID&state_change_only=true" \ + | jq -r '.items[0].status // "unknown"' 2>/dev/null || echo "unknown") + if [ "$DEPLOY_STATUS" != "in progress" ] && [ "$DEPLOY_STATUS" != "pending" ] && [ "$DEPLOY_STATUS" != "executing" ]; then + echo "First deployment finished with status: $DEPLOY_STATUS" + break + fi + echo " Still running ($WAITED s elapsed, status=$DEPLOY_STATUS)..." + sleep 15 + WAITED=$((WAITED + 15)) + done + if [ $WAITED -ge $MAX_WAIT ]; then + echo "Warning: first deployment still running after $MAX_WAIT s — proceeding anyway" + fi + # Second deploy — dns module now reads the real K8s Service port, fixing the + # ingress backend. This is a no-op for apps already routed correctly (e.g. port 3000). + HCTL_EXIT2=0 + timeout 120 hctl score deploy "$PROJECT_ID" "$ENV_ID" score.yaml \ + --no-prompt \ + --default-image "$IMAGE_FULL" || HCTL_EXIT2=$? + if [ "$HCTL_EXIT2" -eq 0 ]; then + echo "✓ Deployment finalised to $ENV_ID" + elif [ "$HCTL_EXIT2" -eq 124 ]; then + echo "✓ Second deployment submitted (polling timed out)" + else + echo "✗ Second hctl deploy failed with exit code $HCTL_EXIT2" + exit $HCTL_EXIT2 + fi + + - name: Deployment Summary + run: | + DEPLOY_URL="https://console.humanitec.dev/orgs/${HUMANITEC_ORG}/projects/$APP_ID" + echo "### 🚀 Deployment Complete" >> $GITHUB_STEP_SUMMARY + echo "| | |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| **Service** | $APP_ID |" >> $GITHUB_STEP_SUMMARY + echo "| **Environment** | $ENV_ID |" >> $GITHUB_STEP_SUMMARY + echo "| **Image** | $IMAGE_FULL |" >> $GITHUB_STEP_SUMMARY + echo "| **Humanitec** | [$DEPLOY_URL]($DEPLOY_URL) |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/techdocs.yml b/.gitea/workflows/techdocs.yml new file mode 100644 index 0000000..e453b72 --- /dev/null +++ b/.gitea/workflows/techdocs.yml @@ -0,0 +1,90 @@ +name: Build and Publish TechDocs + +on: + workflow_run: + workflows: ["Build and Push to ACR"] + branches: [main] + types: [completed] + workflow_dispatch: {} + +env: + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + AZURE_ACCOUNT_NAME: "bstagecjotdevsttechdocs" + ENTITY_NAMESPACE: default + ENTITY_KIND: component + ENTITY_NAME: test-alex-2 + +jobs: + build-and-publish: + if: >- + ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Bootstrap pip + run: | + python3 --version + if python3 -m pip --version 2>/dev/null; then + echo "pip already available" + elif python3 -m ensurepip --version 2>/dev/null; then + python3 -m ensurepip --upgrade + else + apt-get update -qq + apt-get install -y python3-pip + fi + python3 -m pip install --upgrade pip + python3 -m pip --version + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install \ + mkdocs-techdocs-core==1.* \ + mkdocs-git-revision-date-localized-plugin \ + mkdocs-awesome-pages-plugin + + npm install -g @techdocs/cli + + - name: Validate MkDocs config + run: mkdocs build --strict --site-dir /tmp/mkdocs-validate + + - name: Build TechDocs site + run: | + techdocs-cli generate \ + --source-dir . \ + --output-dir ./site \ + --no-docker \ + --verbose + + - name: Install Azure CLI + run: | + if command -v az &>/dev/null; then + echo "Azure CLI already installed: $(az version --query '"azure-cli"' -o tsv)" + else + curl -sL https://aka.ms/InstallAzureCLIDeb | bash + fi + + - name: Azure login (OIDC) + run: | + az login \ + --service-principal \ + --username "$AZURE_CLIENT_ID" \ + --tenant "$AZURE_TENANT_ID" \ + --federated-token "$(cat $AZURE_FEDERATED_TOKEN_FILE)" + + echo "Azure login successful" + + - name: Publish TechDocs site + run: | + techdocs-cli publish \ + --publisher-type azureBlobStorage \ + --storage-name "techdocs" \ + --azureAccountName "$AZURE_ACCOUNT_NAME" \ + --entity "$ENTITY_NAMESPACE/$ENTITY_KIND/$ENTITY_NAME" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e70ae95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +target/ +*.class +*.jar +*.war +*.ear +*.log +.DS_Store +.idea/ +*.iml +*.ipr +*.iws +.vscode/ +.mvn/ +mvnw +mvnw.cmd diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6f8da4a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# ---- Build stage ---- +FROM maven:3.9-eclipse-temurin-17-alpine AS build +WORKDIR /workspace + +COPY pom.xml . +RUN mvn dependency:go-offline -q + +COPY src ./src +RUN mvn package -DskipTests -q + +# ---- Runtime stage ---- +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app + +# Non-root user +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +USER appuser + +COPY --from=build /workspace/target/*.jar app.jar + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD wget -qO- http://localhost:8080/actuator/health | grep -q '"status":"UP"' || exit 1 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb7c8b9 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# test-alex-2 + +test-alex-2 + +**Runtime**: Java 17 · Spring Boot 3.2 +**Owner**: group:default/platform-engineering +**Platform**: Humanitec · Project `test-alex-2` + +## Quick Start + +### Prerequisites +- JDK 17+ +- Maven 3.9+ + +### Run locally +```bash +mvn spring-boot:run +``` + +The service listens on **http://localhost:8080**. + +### Endpoints +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/items` | List all items | +| POST | `/api/items` | Create a new item | +| GET | `/api/items/{id}` | Get item by ID | +| PUT | `/api/items/{id}` | Update item | +| DELETE | `/api/items/{id}` | Delete item | +| GET | `/actuator/health` | Health check | +| GET | `/actuator/prometheus` | Prometheus metrics | + +### Example requests +```bash +# Create an item +curl -X POST http://localhost:8080/api/items \ + -H "Content-Type: application/json" \ + -d '{"name": "Widget", "description": "A demo widget"}' + +# List all items +curl http://localhost:8080/api/items +``` + +## Build +```bash +mvn package -DskipTests +java -jar target/*.jar +``` + +## Container +```bash +docker build -t test-alex-2:dev . +docker run -p 8080:8080 test-alex-2:dev +``` + +## CI/CD + +Push to `main` triggers: +1. **build-push.yml** — Maven build → ACR push +2. **deploy-humanitec.yml** — `humctl score deploy` to `` + +## Links +- [Humanitec Console](https://console.humanitec.dev/orgs/skillful-wild-chicken-2617/projects/test-alex-2) +- [Source Repository](https://gitea.kyndemo.live/demo-platform/test-alex-2) +- [TechDocs](https://backstage.kyndemo.live/docs/default/component/test-alex-2) diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..b883892 --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,152 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-alex-2 + description: test-alex-2 + annotations: + humanitec.dev/orgId: skillful-wild-chicken-2617 + humanitec.dev/projectId: test-alex-2 + backstage.io/techdocs-ref: dir:. + backstage.io/kubernetes-namespace: dev + backstage.io/kubernetes-namespaces: "dev,staging,prod" + backstage.io/kubernetes-label-selector: "app=test-alex-2" + gitea.kyndemo.live/repo-slug: "demo-platform/test-alex-2" + prometheus.io/scrape: "true" + prometheus.io/port: "8080" + prometheus.io/path: "/actuator/prometheus" + grafana/grafana-instance: default + grafana/alert-label-selector: "app=test-alex-2" + grafana/dashboard-selector: "uid == 'app-test-alex-2'" + tags: + - microservice + - golden-path + - java-springboot + - stateless + links: + - url: https://console.humanitec.dev/orgs/skillful-wild-chicken-2617/projects/test-alex-2 + title: Humanitec Console + icon: dashboard + - url: https://grafana.kyndemo.live/d/app-metrics?var-app=test-alex-2 + title: Grafana Dashboard + icon: dashboard + - url: https://gitea.kyndemo.live/demo-platform/test-alex-2 + title: Source Repository + icon: github +spec: + type: service + owner: group:default/platform-engineering + system: system:default/platform-engineering + lifecycle: experimental + providesApis: + - test-alex-2-api +--- +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: test-alex-2-api + description: REST API for test-alex-2 + annotations: + backstage.io/techdocs-ref: dir:. +spec: + type: openapi + lifecycle: experimental + owner: group:default/platform-engineering + system: system:default/platform-engineering + definition: | + openapi: "3.0.0" + info: + title: test-alex-2 + version: "0.1.0" + description: "test-alex-2" + servers: + - url: https://test-alex-2.kyndemo.live + paths: + /api/items: + get: + summary: List all items + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Item' + post: + summary: Create a new item + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ItemRequest' + responses: + "201": + description: Created + /api/items/{id}: + get: + summary: Get item by ID + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + "200": + description: OK + "404": + description: Not found + put: + summary: Update item + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ItemRequest' + responses: + "200": + description: OK + delete: + summary: Delete item + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + "200": + description: Deleted + /actuator/health: + get: + summary: Health check + responses: + "200": + description: UP + components: + schemas: + Item: + type: object + properties: + id: + type: integer + name: + type: string + description: + type: string + ItemRequest: + type: object + properties: + name: + type: string + description: + type: string diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..e79596e --- /dev/null +++ b/docs/api.md @@ -0,0 +1,91 @@ +# API Reference + +Base URL: `https://test-alex-2.kyndemo.live` + +## Items Resource + +### GET /api/items +List all items. + +**Response 200** +```json +[ + { "id": 1, "name": "Widget", "description": "A demo widget" } +] +``` + +--- + +### POST /api/items +Create a new item. + +**Request body** +```json +{ "name": "Widget", "description": "A demo widget" } +``` + +**Response 200** +```json +{ "id": 1, "name": "Widget", "description": "A demo widget" } +``` + +--- + +### GET /api/items/{id} +Get a single item. + +**Path params**: `id` (integer) + +**Response 200** +```json +{ "id": 1, "name": "Widget", "description": "A demo widget" } +``` + +**Response 500** — item not found (throws `NoSuchElementException`) + +--- + +### PUT /api/items/{id} +Update an existing item. + +**Path params**: `id` (integer) + +**Request body** (partial update supported) +```json +{ "name": "Updated Widget" } +``` + +**Response 200** +```json +{ "id": 1, "name": "Updated Widget", "description": "A demo widget" } +``` + +--- + +### DELETE /api/items/{id} +Delete an item. + +**Response 200** +```json +{ "deleted": 1 } +``` + +--- + +## Health & Observability + +### GET /actuator/health +Spring Boot liveness check. + +```json +{ "status": "UP" } +``` + +### GET /actuator/health/liveness +Kubernetes liveness probe target. + +### GET /actuator/health/readiness +Kubernetes readiness probe target. + +### GET /actuator/prometheus +Prometheus-format metrics for scraping by Prometheus or Grafana Agent. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..3f98c02 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,51 @@ +# Architecture + +## Overview + +`test-alex-2` is a stateless microservice built with **Spring Boot 3.2** and **Java 17**, deployed to **AKS** via the **Humanitec** platform using a **Score** workload descriptor. + +## Components + +| Component | Technology | Notes | +|-----------|-----------|-------| +| Web layer | Spring MVC `@RestController` | `ItemsController` | +| Metrics | Micrometer + Prometheus | `/actuator/prometheus` | +| Health | Spring Boot Actuator | Liveness + readiness probes | +| Image registry | Azure Container Registry | Per-environment tags | +| Runtime | AKS (via Humanitec) | Score-driven deployment | + +## Deployment Flow + +``` +Developer pushes to main + │ + ▼ +Gitea Actions: build-push.yml + - mvn package + - docker build + - az acr login (OIDC / Workload Identity) + - docker push → ACR + │ + ▼ +Gitea Actions: deploy-humanitec.yml (triggers on build-push success) + - humctl score deploy + --org skillful-wild-chicken-2617 + --app test-alex-2 + --env + │ + ▼ +Humanitec creates / updates Deployment + │ + ▼ +AKS Pod running test-alex-2 image +``` + +## Security + +- **No static credentials** — CI uses OIDC federated identity to authenticate against Azure ACR +- **Humanitec token** stored in a Gitea repository secret (injected by the golden-path scaffolder) +- **Non-root container** — Dockerfile creates a dedicated `appuser` + +## Scalability + +The Score workload descriptor defines CPU/memory requests/limits. Humanitec and AKS HPA can scale the deployment automatically based on Prometheus metrics. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..760d387 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,46 @@ +# test-alex-2 + +test-alex-2 + +## Overview + +This service was scaffolded from the **Create Microservice** golden-path template on the Kyndryl Platform. + +- **Runtime**: Java 17 · Spring Boot 3.2 +- **Owner**: group:default/platform-engineering +- **Deployment profile**: `stateless` + +## Architecture + +See [architecture.md](architecture.md) for the full architecture diagram. + +At a glance: + +``` +┌──────────────────────────────────────┐ +│ Gitea Actions CI/CD │ +│ ┌─────────────┐ ┌───────────────┐ │ +│ │ build-push │→ │ deploy- │ │ +│ │ .yml │ │ humanitec.yml │ │ +│ └─────────────┘ └───────────────┘ │ +└──────────────────────────────────────┘ + │ │ + ▼ ▼ + Azure ACR Humanitec API + │ + ▼ + AKS (via Score) + │ + ▼ + ┌─────────────────────┐ + │ test-alex-2 │ + │ :8080 │ + │ /api/items │ + │ /actuator/health │ + │ /actuator/prometheus│ + └─────────────────────┘ +``` + +## API Reference + +See [api.md](api.md) for the full OpenAPI reference. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..584e438 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,11 @@ +site_name: test-alex-2 +site_description: test-alex-2 +docs_dir: docs + +nav: + - Home: index.md + - Architecture: architecture.md + - API Reference: api.md + +plugins: + - techdocs-core diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f4c238a --- /dev/null +++ b/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.4 + + + + com.kyndryl.platform + test-alex-2 + 0.0.1-SNAPSHOT + test-alex-2 + test-alex-2 + + + 17 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/score.yaml b/score.yaml new file mode 100644 index 0000000..9ad335f --- /dev/null +++ b/score.yaml @@ -0,0 +1,45 @@ +apiVersion: score.dev/v1b1 +metadata: + name: test-alex-2 + +containers: + app: + image: . + variables: + SPRING_PROFILES_ACTIVE: "dev" + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + resources: + limits: + memory: "512Mi" + cpu: "500m" + requests: + memory: "256Mi" + cpu: "100m" + +service: + ports: + http: + port: 80 + targetPort: 8080 + +resources: + env: + type: environment + + dns: + type: dns + + # ----- Uncomment to add a PostgreSQL database (deployment_profile: db-only or db+cache) ----- + # db: + # type: postgres + + # ----- Uncomment to add Redis cache (deployment_profile: db+cache) ----- + # cache: + # type: redis diff --git a/src/main/java/com/kyndryl/platform/service/Application.java b/src/main/java/com/kyndryl/platform/service/Application.java new file mode 100644 index 0000000..cf09f1a --- /dev/null +++ b/src/main/java/com/kyndryl/platform/service/Application.java @@ -0,0 +1,11 @@ +package com.kyndryl.platform.service; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/main/java/com/kyndryl/platform/service/ItemsController.java b/src/main/java/com/kyndryl/platform/service/ItemsController.java new file mode 100644 index 0000000..35ee232 --- /dev/null +++ b/src/main/java/com/kyndryl/platform/service/ItemsController.java @@ -0,0 +1,70 @@ +package com.kyndryl.platform.service; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * A minimal but fully functional CRUD REST API for "items". + * + * Endpoints: + * GET /api/items — list all items + * POST /api/items — create an item { "name": "...", "description": "..." } + * GET /api/items/{id} — get single item + * PUT /api/items/{id} — update item + * DELETE /api/items/{id} — delete item + * + * Health + metrics are exposed via Spring Actuator at /actuator/health and + * /actuator/prometheus (see application.yml). + */ +@RestController +@RequestMapping("/api/items") +public class ItemsController { + + record Item(long id, String name, String description) {} + + private final Map store = new ConcurrentHashMap<>(); + private final AtomicLong counter = new AtomicLong(1); + + @GetMapping + public Collection list() { + return store.values(); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Item create(@RequestBody Map body) { + long id = counter.getAndIncrement(); + Item item = new Item(id, + body.getOrDefault("name", "unnamed"), + body.getOrDefault("description", "")); + store.put(id, item); + return item; + } + + @GetMapping("/{id}") + public Item get(@PathVariable long id) { + Item item = store.get(id); + if (item == null) throw new NoSuchElementException("Item not found: " + id); + return item; + } + + @PutMapping("/{id}") + public Item update(@PathVariable long id, @RequestBody Map body) { + if (!store.containsKey(id)) throw new NoSuchElementException("Item not found: " + id); + Item updated = new Item(id, + body.getOrDefault("name", store.get(id).name()), + body.getOrDefault("description", store.get(id).description())); + store.put(id, updated); + return updated; + } + + @DeleteMapping("/{id}") + public Map delete(@PathVariable long id) { + store.remove(id); + return Map.of("deleted", id); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..2dccae0 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,21 @@ +server: + port: 8080 + +spring: + application: + name: test-alex-2 + profiles: + active: ${SPRING_PROFILES_ACTIVE:default} + +management: + endpoints: + web: + exposure: + include: health,info,prometheus,metrics + base-path: /actuator + endpoint: + health: + show-details: always + metrics: + tags: + application: test-alex-2 diff --git a/src/test/java/com/kyndryl/platform/service/ApplicationTests.java b/src/test/java/com/kyndryl/platform/service/ApplicationTests.java new file mode 100644 index 0000000..4bb5c5f --- /dev/null +++ b/src/test/java/com/kyndryl/platform/service/ApplicationTests.java @@ -0,0 +1,12 @@ +package com.kyndryl.platform.service; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests { + + @Test + void contextLoads() { + } +}