commit a3227402840adde961d18d69f3e18a66dcf00fb6 Author: Scaffolder Date: Thu Mar 19 16:22:58 2026 +0000 initial commit Change-Id: Iccb9a69a29ce96d618762c5c852c8ec43d9b78df diff --git a/.gitea/workflows/build-push.yml b/.gitea/workflows/build-push.yml new file mode 100644 index 0000000..7b58ca6 --- /dev/null +++ b/.gitea/workflows/build-push.yml @@ -0,0 +1,81 @@ +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: 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-micro-alex-stable-1:${IMAGE_TAG}" + IMAGE_LATEST="${ACR_LOGIN_SERVER}/test-micro-alex-stable-1: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-micro-alex-stable-1 |" >> $GITHUB_STEP_SUMMARY + echo "| **Runtime** | Node.js Express |" >> $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..300ba29 --- /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-micro-alex-stable-1 + PROJECT_ID: test-micro-alex-stable-1 + 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-micro-alex-stable-1:${SHA}" >> $GITHUB_ENV + echo "✓ Image: ${ACR_LOGIN_SERVER}/test-micro-alex-stable-1:${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..c7d3ad4 --- /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-micro-alex-stable-1 + +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..51aa947 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +npm-debug.log* +.env +.DS_Store +dist/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..06cbc62 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# ---- Dependency install stage ---- +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json ./ +RUN npm install --omit=dev + +# ---- Runtime stage ---- +FROM node:20-alpine +WORKDIR /app + +# Non-root user +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +USER appuser + +COPY --from=deps /app/node_modules ./node_modules +COPY package.json ./ +COPY src/ ./src/ + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD wget -qO- http://localhost:3000/health | grep -q 'UP' || exit 1 + +CMD ["node", "src/index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b312736 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# test-micro-alex-stable-1 + +test-micro-alex-stable-1 + +**Runtime**: Node.js 20 · Express 4 +**Owner**: group:default/platform-engineering + +## Quick Start + +```bash +npm install +npm run dev +``` + +Service on **http://localhost:3000**. + +## Endpoints +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/items` | List items | +| POST | `/api/items` | Create item | +| GET | `/api/items/:id` | Get by ID | +| PUT | `/api/items/:id` | Update | +| DELETE | `/api/items/:id` | Delete | +| GET | `/health` | `{"status":"UP"}` | +| GET | `/metrics` | Prometheus metrics | diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..434e94d --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,113 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-micro-alex-stable-1 + description: test-micro-alex-stable-1 + annotations: + humanitec.dev/orgId: skillful-wild-chicken-2617 + humanitec.dev/projectId: test-micro-alex-stable-1 + backstage.io/techdocs-ref: dir:. + backstage.io/kubernetes-namespace: dev + backstage.io/kubernetes-namespaces: "dev,staging,prod" + backstage.io/kubernetes-label-selector: "app=test-micro-alex-stable-1" + gitea.kyndemo.live/repo-slug: "demo-platform/test-micro-alex-stable-1" + prometheus.io/scrape: "true" + prometheus.io/port: "3000" + prometheus.io/path: "/metrics" + grafana/grafana-instance: default + grafana/alert-label-selector: "app=test-micro-alex-stable-1" + grafana/dashboard-selector: "uid == 'app-test-micro-alex-stable-1'" + tags: + - microservice + - golden-path + - nodejs-express + - stateless + links: + - url: https://console.humanitec.dev/orgs/skillful-wild-chicken-2617/projects/test-micro-alex-stable-1 + title: Humanitec Console + icon: dashboard + - url: https://gitea.kyndemo.live/demo-platform/test-micro-alex-stable-1 + title: Source Repository + icon: github +spec: + type: service + owner: group:default/platform-engineering + system: system:default/platform-engineering + lifecycle: experimental + providesApis: + - test-micro-alex-stable-1-api +--- +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: test-micro-alex-stable-1-api + description: REST API for test-micro-alex-stable-1 + 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-micro-alex-stable-1 + version: "0.1.0" + description: "test-micro-alex-stable-1" + servers: + - url: https://test-micro-alex-stable-1.kyndemo.live + paths: + /api/items: + get: + summary: List all items + responses: + "200": + description: OK + post: + summary: Create item + responses: + "201": + description: Created + /api/items/{id}: + get: + summary: Get 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 + responses: + "200": + description: OK + delete: + summary: Delete item + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + "200": + description: Deleted + /health: + get: + summary: Health check + responses: + "200": + description: "{ status: UP }" diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..fe6b371 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,19 @@ +# API Reference + +Base URL: `https://test-micro-alex-stable-1.kyndemo.live` + +## Items `/api/items` + +| Method | Path | Body | Response | +|--------|------|------|----------| +| GET | `/api/items` | — | Array of items | +| POST | `/api/items` | `{name,description}` | Created item (201) | +| GET | `/api/items/:id` | — | Item or 404 | +| PUT | `/api/items/:id` | `{name?,description?}` | Updated item | +| DELETE | `/api/items/:id` | — | `{deleted:id}` | + +## Observability +| Path | Description | +|------|-------------| +| `/health` | `{"status":"UP"}` | +| `/metrics` | Prometheus text format | diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..dd2e9d9 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,16 @@ +# Architecture + +`test-micro-alex-stable-1` is a Node.js 20 **Express** application. + +| Component | Technology | +|-----------|-----------| +| HTTP | Express 4 | +| Metrics | prom-client → `/metrics` | +| Health | `/health` endpoint | + +## Deployment flow + +``` +Push → build-push.yml (npm ci + docker build → ACR) + → deploy-humanitec.yml (humctl score deploy) +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..879af48 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,7 @@ +# Home + +test-micro-alex-stable-1 — test-micro-alex-stable-1 + +- **Runtime**: Node.js 20 · Express 4 +- **Owner**: group:default/platform-engineering +- **Deployment profile**: `stateless` diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..b5941d9 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,11 @@ +site_name: test-micro-alex-stable-1 +site_description: test-micro-alex-stable-1 +docs_dir: docs + +nav: + - Home: index.md + - Architecture: architecture.md + - API Reference: api.md + +plugins: + - techdocs-core diff --git a/package.json b/package.json new file mode 100644 index 0000000..31e9606 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "test-micro-alex-stable-1", + "version": "0.1.0", + "description": "test-micro-alex-stable-1", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js", + "test": "node --test" + }, + "dependencies": { + "express": "^4.19.2", + "prom-client": "^15.1.3" + }, + "engines": { + "node": ">=20" + } +} diff --git a/score.yaml b/score.yaml new file mode 100644 index 0000000..849080a --- /dev/null +++ b/score.yaml @@ -0,0 +1,45 @@ +apiVersion: score.dev/v1b1 +metadata: + name: test-micro-alex-stable-1 + +containers: + app: + image: . + variables: + PORT: "3000" + livenessProbe: + httpGet: + path: /health + port: 3000 + readinessProbe: + httpGet: + path: /health + port: 3000 + resources: + limits: + memory: "256Mi" + cpu: "300m" + requests: + memory: "64Mi" + cpu: "50m" + +service: + ports: + http: + port: 80 + targetPort: 3000 + +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/index.js b/src/index.js new file mode 100644 index 0000000..ed716e6 --- /dev/null +++ b/src/index.js @@ -0,0 +1,78 @@ +/* eslint-env node, es2020 */ +'use strict'; + +const express = require('express'); +const client = require('prom-client'); + +const app = express(); +app.use(express.json()); + +// --------------------------------------------------------------------------- +// Prometheus metrics +// --------------------------------------------------------------------------- +const register = new client.Registry(); +client.collectDefaultMetrics({ register }); + +// --------------------------------------------------------------------------- +// In-memory store +// --------------------------------------------------------------------------- +let nextId = 1; +const store = new Map(); + +// --------------------------------------------------------------------------- +// Health +// --------------------------------------------------------------------------- +app.get('/health', (_req, res) => res.json({ status: 'UP' })); + +// --------------------------------------------------------------------------- +// Metrics +// --------------------------------------------------------------------------- +app.get('/metrics', async (_req, res) => { + res.set('Content-Type', register.contentType); + res.end(await register.metrics()); +}); + +// --------------------------------------------------------------------------- +// Items CRUD +// --------------------------------------------------------------------------- +app.get('/api/items', (_req, res) => { + res.json([...store.values()]); +}); + +app.post('/api/items', (req, res) => { + const { name = 'unnamed', description = '' } = req.body ?? {}; + const item = { id: nextId++, name, description }; + store.set(item.id, item); + res.status(201).json(item); +}); + +app.get('/api/items/:id', (req, res) => { + const item = store.get(Number(req.params.id)); + if (!item) return res.status(404).json({ error: 'Not found' }); + res.json(item); +}); + +app.put('/api/items/:id', (req, res) => { + const id = Number(req.params.id); + const existing = store.get(id); + if (!existing) return res.status(404).json({ error: 'Not found' }); + const updated = { ...existing, ...req.body, id }; + store.set(id, updated); + res.json(updated); +}); + +app.delete('/api/items/:id', (req, res) => { + const id = Number(req.params.id); + store.delete(id); + res.json({ deleted: id }); +}); + +// --------------------------------------------------------------------------- +// Start +// --------------------------------------------------------------------------- +const PORT = Number(process.env.PORT ?? 3000); +app.listen(PORT, () => { + console.log('test-micro-alex-stable-1 listening on :' + PORT); +}); + +module.exports = app; // for testing