diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..ac3cbd2173 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Database Configuration +# These environment variables are used by all services to connect to the database +# For production, use strong passwords and keep this file secure + +DB_USER=postgres +DB_PASSWORD=postgres +DB_HOST=db +DB_PORT=5432 +DB_NAME=postgres + +# Redis Configuration (optional) +REDIS_HOST=redis diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml deleted file mode 100644 index a946a87b03..0000000000 --- a/.github/workflows/call-docker-build-result.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: Build Result -# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml - -on: - # we want pull requests so we can build(test) but not push to image registry - push: - branches: - - 'main' - # only build when important files change - paths: - - 'result/**' - - '.github/workflows/call-docker-build-result.yaml' - pull_request: - branches: - - 'main' - # only build when important files change - paths: - - 'result/**' - - '.github/workflows/call-docker-build-result.yaml' - -jobs: - call-docker-build: - - name: Result Call Docker Build - - uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main - - permissions: - contents: read - packages: write # needed to push docker image to ghcr.io - pull-requests: write # needed to create and update comments in PRs - - secrets: - - # Only needed if with:dockerhub-enable is true below - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} - - # Only needed if with:dockerhub-enable is true below - dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - - with: - - ### REQUIRED - ### ENABLE ONE OR BOTH REGISTRIES - ### tell docker where to push. - ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below - dockerhub-enable: true - ghcr-enable: true - - ### REQUIRED - ### A list of the account/repo names for docker build. List should match what's enabled above - ### defaults to: - image-names: | - ghcr.io/dockersamples/example-voting-app-result - dockersamples/examplevotingapp_result - - ### REQUIRED set rules for tagging images, based on special action syntax: - ### https://github.com/docker/metadata-action#tags-input - ### defaults to: - tag-rules: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=before,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=after,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=pr - - ### path to where docker should copy files into image - ### defaults to root of repository (.) - context: result - - ### Dockerfile alternate name. Default is Dockerfile (relative to context path) - # file: Containerfile - - ### build stage to target, defaults to empty, which builds to last stage in Dockerfile - # target: - - ### platforms to build for, defaults to linux/amd64 - ### other options: linux/amd64,linux/arm64,linux/arm/v7 - platforms: linux/amd64,linux/arm64,linux/arm/v7 - - ### Create a PR comment with image tags and labels - ### defaults to false - # comment-enable: false diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml deleted file mode 100644 index cb4a484a2a..0000000000 --- a/.github/workflows/call-docker-build-vote.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: Build Vote -# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml - -on: - # we want pull requests so we can build(test) but not push to image registry - push: - branches: - - 'main' - # only build when important files change - paths: - - 'vote/**' - - '.github/workflows/call-docker-build-vote.yaml' - pull_request: - branches: - - 'main' - # only build when important files change - paths: - - 'vote/**' - - '.github/workflows/call-docker-build-vote.yaml' - -jobs: - call-docker-build: - - name: Vote Call Docker Build - - uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main - - permissions: - contents: read - packages: write # needed to push docker image to ghcr.io - pull-requests: write # needed to create and update comments in PRs - - secrets: - - # Only needed if with:dockerhub-enable is true below - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} - - # Only needed if with:dockerhub-enable is true below - dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - - with: - - ### REQUIRED - ### ENABLE ONE OR BOTH REGISTRIES - ### tell docker where to push. - ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below - dockerhub-enable: true - ghcr-enable: true - - ### REQUIRED - ### A list of the account/repo names for docker build. List should match what's enabled above - ### defaults to: - image-names: | - ghcr.io/dockersamples/example-voting-app-vote - dockersamples/examplevotingapp_vote - - ### REQUIRED set rules for tagging images, based on special action syntax: - ### https://github.com/docker/metadata-action#tags-input - ### defaults to: - tag-rules: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=before,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=after,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=pr - - ### path to where docker should copy files into image - ### defaults to root of repository (.) - context: vote - - ### Dockerfile alternate name. Default is Dockerfile (relative to context path) - # file: Containerfile - - ### build stage to target, defaults to empty, which builds to last stage in Dockerfile - # target: - - ### platforms to build for, defaults to linux/amd64 - ### other options: linux/amd64,linux/arm64,linux/arm/v7 - platforms: linux/amd64,linux/arm64,linux/arm/v7 - - ### Create a PR comment with image tags and labels - ### defaults to false - # comment-enable: false diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml deleted file mode 100644 index 5abfb6bc9c..0000000000 --- a/.github/workflows/call-docker-build-worker.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: Build Worker -# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml - -on: - # we want pull requests so we can build(test) but not push to image registry - push: - branches: - - 'main' - # only build when important files change - paths: - - 'worker/**' - - '.github/workflows/call-docker-build-worker.yaml' - pull_request: - branches: - - 'main' - # only build when important files change - paths: - - 'worker/**' - - '.github/workflows/call-docker-build-worker.yaml' - -jobs: - call-docker-build: - - name: Worker Call Docker Build - - uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main - - permissions: - contents: read - packages: write # needed to push docker image to ghcr.io - pull-requests: write # needed to create and update comments in PRs - - secrets: - - # Only needed if with:dockerhub-enable is true below - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} - - # Only needed if with:dockerhub-enable is true below - dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - - with: - - ### REQUIRED - ### ENABLE ONE OR BOTH REGISTRIES - ### tell docker where to push. - ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below - dockerhub-enable: true - ghcr-enable: true - - ### REQUIRED - ### A list of the account/repo names for docker build. List should match what's enabled above - ### defaults to: - image-names: | - ghcr.io/dockersamples/example-voting-app-worker - dockersamples/examplevotingapp_worker - - ### REQUIRED set rules for tagging images, based on special action syntax: - ### https://github.com/docker/metadata-action#tags-input - ### defaults to: - tag-rules: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=pr - - ### path to where docker should copy files into image - ### defaults to root of repository (.) - context: worker - - ### Dockerfile alternate name. Default is Dockerfile (relative to context path) - # file: Containerfile - - ### build stage to target, defaults to empty, which builds to last stage in Dockerfile - # target: - - ### platforms to build for, defaults to linux/amd64 - ### other options: linux/amd64,linux/arm64,linux/arm/v7 - # FIXME worker arm/v7 support doesn't build in .net core 3.1 with QEMU - # a fix would likely run the .net build on amd64 but with a target of arm/v7 - platforms: linux/amd64,linux/arm64,linux/arm/v7 - - ### Create a PR comment with image tags and labels - ### defaults to false - # comment-enable: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..b5b66b9df0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,178 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build vote service + uses: docker/build-push-action@v5 + with: + context: ./vote + target: final + push: false + load: true + tags: example-voting-app/vote:test + + - name: Build result service + uses: docker/build-push-action@v5 + with: + context: ./result + push: false + load: true + tags: example-voting-app/result:test + + - name: Build worker service + uses: docker/build-push-action@v5 + with: + context: ./worker + push: false + load: true + tags: example-voting-app/worker:test + + - name: Verify Docker images were built + run: | + docker image ls | grep example-voting-app + + - name: Start services with Docker Compose + run: docker compose up -d + env: + COMPOSE_PROJECT_NAME: ci-test + + - name: Wait for services to be healthy + run: | + echo "Waiting for services to start..." + sleep 10 + docker compose ps + + - name: Check vote service connectivity + run: | + docker compose exec -T vote curl -f http://localhost/ || true + + - name: Check result service connectivity + run: | + docker compose exec -T result curl -f http://localhost/ || true + + - name: Test vote endpoint + run: | + # Test if vote app responds + VOTE_PORT=8080 + max_attempts=30 + attempt=0 + + while [ $attempt -lt $max_attempts ]; do + if curl -f http://localhost:$VOTE_PORT/ > /dev/null 2>&1; then + echo "Vote service is responding" + exit 0 + fi + attempt=$((attempt + 1)) + echo "Attempt $attempt/$max_attempts - waiting for vote service..." + sleep 1 + done + + echo "Vote service failed to respond" + exit 1 + + - name: Test result endpoint + run: | + # Test if result app responds + RESULT_PORT=8081 + max_attempts=30 + attempt=0 + + while [ $attempt -lt $max_attempts ]; do + if curl -f http://localhost:$RESULT_PORT/ > /dev/null 2>&1; then + echo "Result service is responding" + exit 0 + fi + attempt=$((attempt + 1)) + echo "Attempt $attempt/$max_attempts - waiting for result service..." + sleep 1 + done + + echo "Result service failed to respond" + exit 1 + + - name: Verify database connectivity + run: | + docker compose exec -T db psql -U postgres -c "SELECT 1" || true + + - name: Check Docker Compose logs + if: always() + run: docker compose logs --tail=50 + + - name: Stop services + if: always() + run: docker compose down -v + + lint-python: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install linting tools + run: | + pip install pylint flake8 + + - name: Lint vote service + run: | + flake8 vote/app.py --max-line-length=120 --ignore=E501,W503 || true + pylint vote/app.py --disable=all --enable=E,F || true + + lint-javascript: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install result dependencies + run: cd result && npm ci + + - name: Check for npm vulnerabilities + run: cd result && npm audit --audit-level=moderate || true + + lint-dotnet: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '7.0' + + - name: Restore worker dependencies + run: cd worker && dotnet restore + + - name: Build worker + run: cd worker && dotnet build --no-restore --configuration Release + + - name: Check for code style issues + run: cd worker && dotnet format --verify-no-changes --verbosity diagnostic || true diff --git a/.gitignore b/.gitignore index 9a6f694990..f8d8e3ae4f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,9 @@ project.lock.json bin/ obj/ .vs/ -node_modules/ +node_modules + +# Environment files with secrets +.env +.env.local +.env.*.local/ diff --git a/docker-compose.images.yml b/docker-compose.images.yml index 8909aae794..633b4e95d7 100644 --- a/docker-compose.images.yml +++ b/docker-compose.images.yml @@ -50,8 +50,9 @@ services: db: image: postgres:15-alpine environment: - POSTGRES_USER: "postgres" - POSTGRES_PASSWORD: "postgres" + POSTGRES_USER: "${DB_USER:-postgres}" + POSTGRES_PASSWORD: "${DB_PASSWORD:-postgres}" + POSTGRES_DB: "postgres" volumes: - "db-data:/var/lib/postgresql/data" - "./healthchecks:/healthchecks" diff --git a/docker-compose.yml b/docker-compose.yml index 5915ffd741..8fc8f6349b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,13 @@ services: entrypoint: nodemon --inspect=0.0.0.0 server.js depends_on: db: - condition: service_healthy + condition: service_healthy + environment: + DB_HOST: "db" + DB_PORT: "5432" + DB_USER: "${DB_USER:-postgres}" + DB_PASSWORD: "${DB_PASSWORD:-postgres}" + DB_NAME: "postgres" volumes: - ./result:/usr/local/app ports: @@ -45,9 +51,15 @@ services: context: ./worker depends_on: redis: - condition: service_healthy + condition: service_healthy db: - condition: service_healthy + condition: service_healthy + environment: + DB_HOST: "db" + DB_PORT: "5432" + DB_USER: "${DB_USER:-postgres}" + DB_PASSWORD: "${DB_PASSWORD:-postgres}" + DB_NAME: "postgres" networks: - back-tier @@ -64,8 +76,9 @@ services: db: image: postgres:15-alpine environment: - POSTGRES_USER: "postgres" - POSTGRES_PASSWORD: "postgres" + POSTGRES_USER: "${DB_USER:-postgres}" + POSTGRES_PASSWORD: "${DB_PASSWORD:-postgres}" + POSTGRES_DB: "postgres" volumes: - "db-data:/var/lib/postgresql/data" - "./healthchecks:/healthchecks" diff --git a/docker-stack.yml b/docker-stack.yml index 356b944caf..f725d61cf2 100644 --- a/docker-stack.yml +++ b/docker-stack.yml @@ -14,8 +14,9 @@ services: db: image: postgres:15-alpine environment: - POSTGRES_USER: "postgres" - POSTGRES_PASSWORD: "postgres" + POSTGRES_USER: "${DB_USER:-postgres}" + POSTGRES_PASSWORD: "${DB_PASSWORD:-postgres}" + POSTGRES_DB: "postgres" volumes: - db-data:/var/lib/postgresql/data networks: diff --git a/k8s-specifications/db-credentials-secret.yaml b/k8s-specifications/db-credentials-secret.yaml new file mode 100644 index 0000000000..93af3207c1 --- /dev/null +++ b/k8s-specifications/db-credentials-secret.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Secret +metadata: + name: db-credentials +type: Opaque +stringData: + username: postgres + password: postgres + +--- +# NOTE: For production use, create this secret separately using: +# kubectl create secret generic db-credentials \ +# --from-literal=username= \ +# --from-literal=password= +# +# Do NOT commit actual production credentials to version control! +# Use Kubernetes Secrets management or a secrets operator like Sealed Secrets or External Secrets. diff --git a/k8s-specifications/db-deployment.yaml b/k8s-specifications/db-deployment.yaml index bc94ca7368..7288437473 100644 --- a/k8s-specifications/db-deployment.yaml +++ b/k8s-specifications/db-deployment.yaml @@ -19,8 +19,16 @@ spec: name: postgres env: - name: POSTGRES_USER - value: postgres + valueFrom: + secretKeyRef: + name: db-credentials + key: username - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: db-credentials + key: password + - name: POSTGRES_DB value: postgres ports: - containerPort: 5432 diff --git a/result/Dockerfile b/result/Dockerfile index 33f0ba86fb..189cd0ff71 100644 --- a/result/Dockerfile +++ b/result/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-slim +FROM node:22-slim # add curl for healthcheck RUN apt-get update && \ diff --git a/result/server.js b/result/server.js index 1c8593e7ee..39904c1fe3 100644 --- a/result/server.js +++ b/result/server.js @@ -17,8 +17,14 @@ io.on('connection', function (socket) { }); }); +var dbUser = process.env.DB_USER || 'postgres'; +var dbPassword = process.env.DB_PASSWORD || 'postgres'; +var dbHost = process.env.DB_HOST || 'db'; +var dbPort = process.env.DB_PORT || '5432'; +var dbName = process.env.DB_NAME || 'postgres'; + var pool = new Pool({ - connectionString: 'postgres://postgres:postgres@db/postgres' + connectionString: `postgres://${dbUser}:${dbPassword}@${dbHost}:${dbPort}/${dbName}` }); async.retry( diff --git a/result/tests/Dockerfile b/result/tests/Dockerfile index b8b6e90520..60ae3c17e2 100644 --- a/result/tests/Dockerfile +++ b/result/tests/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8.9-slim +FROM node:22-slim RUN apt-get update -qq && apt-get install -qy \ ca-certificates \ diff --git a/seed-data/Dockerfile b/seed-data/Dockerfile index f970e42ad4..fc3bb6d19b 100644 --- a/seed-data/Dockerfile +++ b/seed-data/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim +FROM python:3.12-slim # add apache bench (ab) tool RUN apt-get update \ diff --git a/vote/Dockerfile b/vote/Dockerfile index 2681083600..bcd14f70ba 100644 --- a/vote/Dockerfile +++ b/vote/Dockerfile @@ -1,5 +1,5 @@ # base defines a base stage that uses the official python runtime base image -FROM python:3.11-slim AS base +FROM python:3.12-slim AS base # Add curl for healthcheck RUN apt-get update && \ diff --git a/worker/Dockerfile b/worker/Dockerfile index a3f92d7e94..17315666fe 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -8,7 +8,7 @@ # docker buildx build --platform "linux/arm64/v8" . # build compiles the program for the builder's local platform -FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG TARGETPLATFORM ARG TARGETARCH ARG BUILDPLATFORM @@ -22,7 +22,7 @@ COPY . . RUN dotnet publish -c release -o /app -a $TARGETARCH --self-contained false --no-restore # app image -FROM mcr.microsoft.com/dotnet/runtime:7.0 +FROM mcr.microsoft.com/dotnet/runtime:8.0 WORKDIR /app COPY --from=build /app . ENTRYPOINT ["dotnet", "Worker.dll"] diff --git a/worker/Program.cs b/worker/Program.cs index 9b5fb74d1a..4e6fb1da69 100644 --- a/worker/Program.cs +++ b/worker/Program.cs @@ -16,8 +16,15 @@ public static int Main(string[] args) { try { - var pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;"); - var redisConn = OpenRedisConnection("redis"); + var dbHost = Environment.GetEnvironmentVariable("DB_HOST") ?? "db"; + var dbPort = Environment.GetEnvironmentVariable("DB_PORT") ?? "5432"; + var dbUser = Environment.GetEnvironmentVariable("DB_USER") ?? "postgres"; + var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "postgres"; + var redisHost = Environment.GetEnvironmentVariable("REDIS_HOST") ?? "redis"; + + var connectionString = $"Server={dbHost};Port={dbPort};Username={dbUser};Password={dbPassword};"; + var pgsql = OpenDbConnection(connectionString); + var redisConn = OpenRedisConnection(redisHost); var redis = redisConn.GetDatabase(); // Keep alive is not implemented in Npgsql yet. This workaround was recommended: @@ -46,7 +53,7 @@ public static int Main(string[] args) if (!pgsql.State.Equals(System.Data.ConnectionState.Open)) { Console.WriteLine("Reconnecting DB"); - pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;"); + pgsql = OpenDbConnection(connectionString); } else { // Normal +1 vote requested diff --git a/worker/Worker.csproj b/worker/Worker.csproj index 00845078ef..d0e89acdd3 100644 --- a/worker/Worker.csproj +++ b/worker/Worker.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0