diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml index 563e584..cb55f42 100755 --- a/.devcontainer/compose.yaml +++ b/.devcontainer/compose.yaml @@ -10,9 +10,6 @@ networks: # Service top-level element reference: https://docs.docker.com/compose/compose-file/05-services/ services: devcontainer: - depends_on: - demoapp-backend: - condition: service_healthy image: mcr.microsoft.com/devcontainers/base:bullseye environment: # Enable Docker BuildKit https://docs.docker.com/build/buildkit/ @@ -24,50 +21,6 @@ services: # Red Hat runtime image already exposes port 8080, thus `expose` keyword can be omitted # expose: # - "8080" - networks: - - backend # Connect to `backend` network - - frontend # Connect to `frontend` network - demoapp-backend: - depends_on: - mysql: - condition: service_healthy # healthy status is indicated by `healthcheck` keyword - image: ${DEMOAPP_BACKEND_IMAGE} # defined in .env file - environment: - # Externalized Spring Configuration: https://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/html/boot-features-external-config.html - SPRING_DATASOURCE_USERNAME: root - SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD} # defined in .env file - healthcheck: - # Check Spring Boot Actuator - test: curl --fail http://demoapp-backend:8080/actuator/health # command for testing health - # Specifying durations: https://docs.docker.com/compose/compose-file/11-extension/#specifying-durations - interval: 5s # specifies the time duration or interval in which the healthcheck process will execute - timeout: 1s # defines the time duration to wait for a healthcheck - retries: 10 # is used to define the number of tries to implement the health check after failure - networks: - - backend # Connect to `backend` network - - frontend # Connect to `frontend` network - restart: always - mysql: # Service name is also used as hostname when connecting from other containers - image: mysql:8.0 # https://hub.docker.com/_/mysql - environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} # defined in .env file - MYSQL_DATABASE: ${MYSQL_DATABASE} # defined in .env file - MYSQL_USER: ${MYSQL_USER} # defined in .env file - MYSQL_PASSWORD: ${MYSQL_PASSWORD} # defined in .env file - healthcheck: - # Login to mysql demoapp db - test: mysql --host=localhost --user=root --password=$$MYSQL_ROOT_PASSWORD demoapp # command for testing health - # Specifying durations: https://docs.docker.com/compose/compose-file/11-extension/#specifying-durations - interval: 5s # specifies the time duration or interval in which the healthcheck process will execute - timeout: 1s # defines the time duration to wait for a healthcheck - retries: 10 # is used to define the number of tries to implement the health check after failure - restart: unless-stopped - volumes: - - type: volume - source: mysql-data - target: /var/lib/mysql - networks: - - backend # Connect to `backend` network # Volumes top-level element reference: https://docs.docker.com/compose/compose-file/07-volumes/ volumes: diff --git a/.env b/.env index db9db1c..f640e82 100644 --- a/.env +++ b/.env @@ -1,2 +1,4 @@ +# Default environment configuration when application is started inside devcontainer +REACT_APP_DEMOAPP_BACKEND_URL=http://localhost:8080 HOST=0.0.0.0 PORT=8080 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..81c7b58 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..36014cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'enhancement' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/actions/container-structure-test/Dockerfile b/.github/actions/container-structure-test/Dockerfile new file mode 100644 index 0000000..b2e735f --- /dev/null +++ b/.github/actions/container-structure-test/Dockerfile @@ -0,0 +1,9 @@ +FROM alpine:3.18 + +# Get VERSION from: https://github.com/GoogleContainerTools/container-structure-test/releases +ARG VERSION=latest + +RUN apk add --no-cache curl + +# Get command from: https://github.com/GoogleContainerTools/container-structure-test#linux +RUN curl -LO https://storage.googleapis.com/container-structure-test/${VERSION}/container-structure-test-linux-amd64 && chmod +x container-structure-test-linux-amd64 && mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test diff --git a/.github/actions/container-structure-test/action.yml b/.github/actions/container-structure-test/action.yml new file mode 100644 index 0000000..651438e --- /dev/null +++ b/.github/actions/container-structure-test/action.yml @@ -0,0 +1,25 @@ +--- +name: Container Structure Tests +description: | + Container Structure Tests provide a powerful framework to validate the structure of a container image. + These tests can be used to check the output of commands in an image, as well as verify metadata and contents of the filesystem. + See https://github.com/GoogleContainerTools/container-structure-test + Note: This action does not pull remote images +inputs: + image: + description: Container Image to test + required: true + configFile: + description: Path to Container Structure Test Configuration File + required: false + default: default-container-structure-test.yaml +runs: + using: docker + image: Dockerfile + args: + - container-structure-test + - test + - --image + - ${{ inputs.image }} + - --config + - ${{ inputs.configFile }} diff --git a/.github/actions/container-structure-test/default-container-structure-test.yaml b/.github/actions/container-structure-test/default-container-structure-test.yaml new file mode 100644 index 0000000..3f9dd4f --- /dev/null +++ b/.github/actions/container-structure-test/default-container-structure-test.yaml @@ -0,0 +1,15 @@ +--- +# Run command: +# > container-structure-test test --image demoapp-frontend --config container-structure-test.yaml +schemaVersion: 2.0.0 + +metadataTest: + user: '' + +fileExistenceTests: + - name: Check Container structure test is installed + path: /usr/local/bin/container-structure-test + shouldExist: true + permissions: -rwxr-xr-x + uid: 0 + gid: 0 diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..54e11f8 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,67 @@ +--- +# actions/labeler configuration: https://github.com/marketplace/actions/labeler + +# Default GitHub Labels: https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels#about-default-labels + +# Add `documentation` label +documentation: + # to any changes of markdown files on any folder or subfolders + - '**/*.md' + # to any changes within docs folder + - docs/** + +# Add `javascript` label +javascript: + # to any changes within `src` folder + - src/** + # to any changes within `public` folder + - public/** + +# Add `dependencies` label +dependencies: + - package.json + - package-lock.json + + +# Add `container` label +container: + # to any changes of any Containerfile within this repository + - '**/Containerfile*' + # to any changes of any Dockerfile within this repository + - '**/Dockerfile*' + # to any chages to container-structure-test.yaml file + - container-structure-test.yaml + # to any chages to compose.yaml or compose.yml file + - compose.yaml + - compose.yml + +# Add `devcontainer` label +devcontainer: + # to any changes within .devcontainer folder + - .devcontainer/** + +# Add `github-workflow` label +github-workflow: + # to any changes within .github folder + - .github/** + +# Add `vscode-settings` label +vscode-settings: + # to any changes within .vscode folder + - .vscode/** + +# Add `git-config` label +git-config: + # to any changes within .git folder + - .git/** + # to any changes to .gitattributes file + - .gitattributes + # to any changes to .gitignore file + - .gitignore + # to any changes within .githooks folder + - .githooks/** + +# Add `lint` label +lint: + # to any changes to files, folders and subfolders with `lint` keyword + - '**lint**' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ad2df8c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ + +Brief description of why this PR is necessary and/or what this PR solves. + +- Fixes [ISSUE #1] +- Fixes [ISSUE #2] + +Checklist: +* [ ] The title of this PR states what changed and the related issues number (used for the release note). +* [ ] The description of this PR includes a brief description of why this PR is necessary and/or what this PR solves. +* [ ] The description of this PR includes "Fixes [ISSUE #]" to automatically close associated issues. +* [ ] The changes in this PR are accompanied by documentation. +* [ ] The changes in this PR include unit and/or e2e tests. PRs without these are unlikely to be merged. diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..2c772e3 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,45 @@ +--- +# Configuration for .github/workflows/release-drafter.yml + +name-template: 'v$RESOLVED_VERSION 📢' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + # label: 'chore' + labels: + - documentation # label from .github/labeler.yml +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + - java # label from .github/labeler.yml + minor: + labels: + - 'minor' + - dependencies # label from .github/labeler.yml + - npm # label from .github/labeler.yml + - gradle # label from .github/labeler.yml + patch: + labels: + - 'patch' + default: patch +template: | + ## Changes + + $CHANGES + + ## Install from the command line + ```sh + DEMOAPP_BACKEND_IMAGE="ghcr.io/$OWNER/$REPOSITORY:v$RESOLVED_VERSION" docker compose --project-directory deploy/docker-compose up + ``` diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..5c7c810 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,41 @@ +--- +# Setup automatically generated release notes +# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes + +changelog: + exclude: + labels: + - ignore-for-release + categories: + - title: Breaking Changes 🛠 + labels: + - Semver-Major + - breaking-change + - title: Exciting New Features 🎉 + labels: + - Semver-Minor + - enhancement + - title: Documentation Improvements + labels: + - documentation + - title: Java Changes + labels: + - java + - title: Dependency Changes + labels: + - dependencies + - npm + - gradle + - title: Container Changes + labels: + - container + - title: Local Development Changes + labels: + - devcontainer + - vscode-settings + - title: Workflow Changes + labels: + - github-workflow + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ac74138 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,184 @@ +--- +# Workflow syntax for GitHub Actions: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +# Build Application and Upload Container Image to Docker Hub +name: Build and Scan Image + +# Events: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +on: + # Run workflow on push except for ignored branches and paths + push: + # Secrets aren't available for dependabot on push. https://docs.github.com/en/enterprise-cloud@latest/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/troubleshooting-the-codeql-workflow#error-403-resource-not-accessible-by-integration-when-using-dependabot + branches-ignore: + # - 'dependabot/**' + - 'cherry-pick-*' + paths-ignore: + - '**.md' # Ignore documentation changes + - '.github/**(!build.yml)' # Ignore other workflow changes + # Run workflow on pull request + pull_request: # By default, a workflow only runs when a pull_request event's activity type is opened, synchronize, or reopened + # Allow user to manually trigger Workflow execution + workflow_dispatch: + +# Set Workflow-level permissions: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: + contents: read + +# Run a single job at a time: https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Set Workflow-level environment variables +env: + PROJECT: demoapp-frontend + +jobs: + build: + # Run job when not triggered by a merge + if: (github.event_name == 'push' && contains(toJSON(github.event.head_commit.message), 'Merge pull request ') == false) || (github.event_name != 'push') + runs-on: ubuntu-latest + environment: docker-hub # Use `docker-hub` repository environment + # Uncomment lines below to run `build` job on container + # Note: container image must contains commands required for step execution, e.g. docker, gzip, etc. + # container: + # image: mcr.microsoft.com/openjdk/jdk:17-ubuntu # Image Java version must match with `project.version` in pom.xml + # # Set credentials when container registry requires authentication to pull the image + # # credentials: + # # username: ${{ github.actor }} + # # password: ${{ secrets.github_token }} + steps: + # Workaround for the absence of github.branch_name + # Setting an environment variable: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable + - name: Set VERSION + if: github.head_ref != '' + run: | + echo "VERSION=${{ github.head_ref }}" >> $GITHUB_ENV + - name: Set VERSION + if: github.head_ref == '' + run: | + echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + + # Set Complete Container Image URL + - name: Set CONTAINER_IMAGE_URL + run: | + echo "CONTAINER_IMAGE_URL=${{ vars.DOCKER_REGISTRY_URL }}/${{ vars.DOCKER_REPOSITORY }}/${{ env.PROJECT }}:${{ env.VERSION }}" >> $GITHUB_ENV + + - name: Checkout repository + uses: actions/checkout@v4 # https://github.com/marketplace/actions/checkout + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 # https://github.com/marketplace/actions/docker-setup-build + + - name: Login to DockerHub + uses: docker/login-action@v3 # https://github.com/marketplace/actions/docker-login + with: + registry: ${{ vars.DOCKER_REGISTRY_URL }} + username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} + password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} + + - name: Docker Build and Push + uses: docker/build-push-action@v5 # https://github.com/marketplace/actions/build-and-push-docker-images + with: + context: . + file: Containerfile + push: true + tags: ${{ env.CONTAINER_IMAGE_URL }} # CONTAINER_IMAGE_URL is defined in GITHUB_ENV + cache-from: type=gha + cache-to: type=gha,mode=max + + container-structure-test: + needs: build + runs-on: ubuntu-latest + environment: docker-hub # Use `docker-hub` repository environment + steps: + # Workaround for the absence of github.branch_name + # Setting an environment variable: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable + - name: Set VERSION + if: github.head_ref != '' + run: | + echo "VERSION=${{ github.head_ref }}" >> $GITHUB_ENV + - name: Set VERSION + if: github.head_ref == '' + run: | + echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + + # Set Complete Container Image URL + - name: Set CONTAINER_IMAGE_URL + run: | + echo "CONTAINER_IMAGE_URL=${{ vars.DOCKER_REGISTRY_URL }}/${{ vars.DOCKER_REPOSITORY }}/${{ env.PROJECT }}:${{ env.VERSION }}" >> $GITHUB_ENV + + - name: Login to DockerHub + uses: docker/login-action@v3 # https://github.com/marketplace/actions/docker-login + with: + registry: ${{ vars.DOCKER_REGISTRY_URL }} + username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} + password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} + - name: Pull Container Image + # CONTAINER_IMAGE_URL is defined in GITHUB_ENV + run: | + docker pull ${{ env.CONTAINER_IMAGE_URL }} + + - name: Checkout repository + uses: actions/checkout@v4 # https://github.com/marketplace/actions/checkout + + - name: Run Container Structure Test + uses: ./.github/actions/container-structure-test + with: + image: ${{ env.CONTAINER_IMAGE_URL }} # CONTAINER_IMAGE_URL is defined in GITHUB_ENV + configFile: ./container-structure-test.yaml + + scan: + needs: build + runs-on: ubuntu-latest + # Set Job-level permissions: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idpermissions + permissions: + security-events: write # Allow Job to upload scan results to GitHub + environment: docker-hub # Use `docker-hub` repository environment + env: + TRIVY_CACHE_DIR: /tmp/trivy/ + steps: + # Workaround for the absence of github.branch_name + # Setting an environment variable: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable + - name: Set VERSION + if: github.head_ref != '' + run: | + echo "VERSION=${{ github.head_ref }}" >> $GITHUB_ENV + - name: Set VERSION + if: github.head_ref == '' + run: | + echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + + # Set Complete Container Image URL + - name: Set CONTAINER_IMAGE_URL + run: | + echo "CONTAINER_IMAGE_URL=${{ vars.DOCKER_REGISTRY_URL }}/${{ vars.DOCKER_REPOSITORY }}/${{ env.PROJECT }}:${{ env.VERSION }}" >> $GITHUB_ENV + + - name: Checkout repository + uses: actions/checkout@v4 # https://github.com/marketplace/actions/checkout + + - name: Cache Trivy + id: cache + uses: actions/cache@v3 # https://github.com/marketplace/actions/cache#using-a-combination-of-restore-and-save-actions + with: + path: ${{ env.TRIVY_CACHE_DIR }} + key: trivy-${{ hashFiles('**/package-lock.json', '**/Containerfile*') }} # Trivy scan results are influenced by npm dependencies and Containerfile runtime image + + - name: Scan Image with Aqua Security Trivy + uses: aquasecurity/trivy-action@0.13.0 # https://github.com/marketplace/actions/aqua-security-trivy + with: + image-ref: ${{ env.CONTAINER_IMAGE_URL }} # CONTAINER_IMAGE_URL is defined in GITHUB_ENV + vuln-type: 'os,library' + severity: 'LOW,MEDIUM,HIGH,CRITICAL' + scanners: 'vuln,secret,config' + ignore-unfixed: true + exit-code: '1' + cache-dir: ${{ env.TRIVY_CACHE_DIR }} + format: sarif + output: 'trivy-results.sarif' + env: + TRIVY_USERNAME: ${{ secrets.DOCKER_REGISTRY_USERNAME }} + TRIVY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2.22.5 # https://github.com/github/codeql-action/tree/main/upload-sarif + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/code-scan-codecov.yml b/.github/workflows/code-scan-codecov.yml new file mode 100644 index 0000000..7d0655e --- /dev/null +++ b/.github/workflows/code-scan-codecov.yml @@ -0,0 +1,40 @@ +--- +# Workflow syntax for GitHub Actions: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +name: Scan Code with CodeCov + +# Events: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +on: + # Run workflow on push except for ignored branches and paths + push: + # Secrets aren't available for dependabot on push. https://docs.github.com/en/enterprise-cloud@latest/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/troubleshooting-the-codeql-workflow#error-403-resource-not-accessible-by-integration-when-using-dependabot + branches-ignore: + - 'dependabot/**' + - 'cherry-pick-*' + paths-ignore: + - '**.md' # Ignore documentation changes + - '.github/**(!code-scan-codecov.yml)' # Ignore other workflow changes + # Run workflow on pull request + pull_request: # By default, a workflow only runs when a pull_request event's activity type is opened, synchronize, or reopened + +# Run a single job at a time: https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Set Workflow-level permissions: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: + contents: read + +jobs: + codecov: + # Run job when not triggered by a merge + if: (github.event_name == 'push' && contains(toJSON(github.event.head_commit.message), 'Merge pull request ') == false) || (github.event_name != 'push') + runs-on: ubuntu-latest # GitHub-hosted runners: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + steps: + - name: Checkout repository + uses: actions/checkout@v4 # https://github.com/marketplace/actions/checkout + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/code-scan-codeql.yml b/.github/workflows/code-scan-codeql.yml new file mode 100644 index 0000000..652e8be --- /dev/null +++ b/.github/workflows/code-scan-codeql.yml @@ -0,0 +1,60 @@ +--- +# Workflow syntax for GitHub Actions: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +# See CodeQL results at https://github.com/paul-gilber/demoapp-frontend/security/code-scanning/tools/CodeQL/status/ +name: Scan Code with CodeQL + +# Events: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +on: + # Run workflow on push except for ignored branches and paths + push: + # Secrets aren't available for dependabot on push. https://docs.github.com/en/enterprise-cloud@latest/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/troubleshooting-the-codeql-workflow#error-403-resource-not-accessible-by-integration-when-using-dependabot + branches-ignore: + - 'dependabot/**' + - 'cherry-pick-*' + paths-ignore: + - '**.md' # Ignore documentation changes + - '.github/**(!code-scan-codeql.yml)' # Ignore other workflow changes + # Run workflow on pull request + pull_request: # By default, a workflow only runs when a pull_request event's activity type is opened, synchronize, or reopened +# Run a single job at a time: https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Set Workflow-level permissions: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: + contents: read + +jobs: + codeql: + # Run job when not triggered by a merge + if: (github.event_name == 'push' && contains(toJSON(github.event.head_commit.message), 'Merge pull request ') == false) || (github.event_name != 'push') + runs-on: ubuntu-latest # GitHub-hosted runners: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + permissions: + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/autobuild to send a status report + steps: + - name: Checkout repository + uses: actions/checkout@v4 # https://github.com/marketplace/actions/checkout + + # Initializes CodeQL scanning tools + # https://github.com/github/codeql-action + - name: Initialize CodeQL + uses: github/codeql-action/init@v2.22.5 # https://github.com/github/codeql-action + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # id: autobuild + # uses: github/codeql-action/autobuild@v2.22.5 + + # Note: If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + - name: Build + run: | + npm ci + + - name: Run CodeQL Analysis + uses: github/codeql-action/analyze@v2.22.5 diff --git a/.github/workflows/code-scan-sonarcloud.yml b/.github/workflows/code-scan-sonarcloud.yml new file mode 100644 index 0000000..88e7379 --- /dev/null +++ b/.github/workflows/code-scan-sonarcloud.yml @@ -0,0 +1,148 @@ +--- +# Workflow syntax for GitHub Actions: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +# SonarCloud: https://sonarcloud.io/ +# CI analysis while Automatic Analysis must be disabled for successful execution of this workflow https://docs.sonarcloud.io/advanced-setup/automatic-analysis/#conflict-with-ci-based-analysis +name: Scan Code with SonarCloud + +# Events: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +on: + # Run workflow on push except for ignored branches and paths + push: + paths-ignore: + - '**.md' # Ignore documentation changes + - '.github/**(!code-scan-sonarcloud.yml)' # Ignore other workflow changes + # Run workflow on pull request + pull_request: # By default, a workflow only runs when a pull_request event's activity type is opened, synchronize, or reopened + +# Run a single job at a time: https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Set Workflow-level permissions: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: + contents: read + +jobs: + sonarcloud: + # Run job when not triggered by a merge + if: (github.event_name == 'push' && contains(toJSON(github.event.head_commit.message), 'Merge pull request ') == false) || (github.event_name != 'push') + runs-on: ubuntu-latest # GitHub-hosted runners: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + # Set Job-level permissions: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs + permissions: + pull-requests: read # Allow SonarCloud to get pull request details + environment: sonarcloud # Use `sonarcloud` repository environment + steps: + # Workaround for the absence of github.branch_name + # Setting an environment variable: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable + - name: Set VERSION + if: github.head_ref != '' + run: | + echo "VERSION=${{ github.head_ref }}" >> $GITHUB_ENV + - name: Set VERSION + if: github.head_ref == '' + run: | + echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + + - name: Checkout repository + uses: actions/checkout@v4 # https://github.com/marketplace/actions/checkout + with: + # Disabling shallow clone is recommended for improving relevancy of reporting + fetch-depth: 0 + + - name: Cache SonarCloud dependencies + uses: actions/cache@v3 # https://github.com/marketplace/actions/cache#using-a-combination-of-restore-and-save-actions + with: + path: | + ~/.sonar/cache + key: sonarcloud-${{ github.repository_id }} + + - name: SonarCloud Scan via Github Action + uses: sonarsource/sonarcloud-github-action@v2.0.2 # https://github.com/marketplace/actions/sonarcloud-scan + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN is a special secret automatically generated by GitHub: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # SONAR_TOKEN must be defined in `sonarcloud` repository environment. SonarCloud access token should be generated from https://sonarcloud.io/account/security/ + with: + projectBaseDir: src + args: > + -Dsonar.organization=${{ vars.SONAR_ORGANIZATION }} + -Dsonar.projectKey=${{ vars.SONAR_PROJECT_KEY }} + + # In case you need to override default settings + # - name: Analyze with SonarCloud + # uses: sonarsource/sonarcloud-github-action@v2.0.2 + # with: + # projectBaseDir: my-custom-directory + # args: > + # -Dsonar.organization=my-organization + # -Dsonar.projectKey=my-projectkey + # -Dsonar.python.coverage.reportPaths=coverage.xml + # -Dsonar.sources=lib/ + # -Dsonar.test.exclusions=tests/** + # -Dsonar.tests=tests/ + # -Dsonar.verbose=true + + # # SonarCloud GitHub Action fails when a NPM project is detected and recommends usage of NPM Sonar plugin + # - name: SonarCloud Scan via NPM (${{ github.event_name }}) + # if: github.event_name != 'pull_request' + # # Get SONAR_ORGANIZATION and SONAR_PROJECT_KEY from https://sonarcloud.io/project/information?id=paul-gilber_demoapp-frontend + # # SONAR_ORGANIZATION must be defined in `sonarcloud` repository environment + # # SONAR_PROJECT_KEY must be defined in `sonarcloud` repository environment + # run: | + # mvn -B verify \ + # org.sonarsource.scanner.npm:sonar-npm-plugin:sonar \ + # -Drevision=${{ env.VERSION }} \ + # -Dsonar.organization=${{ vars.SONAR_ORGANIZATION }} \ + # -Dsonar.projectKey=${{ vars.SONAR_PROJECT_KEY }} \ + # -Dmaven.test.skip=true \ + # -Ddockerfile.skip=true + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN is a special secret automatically generated by GitHub: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret + # SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} # SONAR_HOST_URL must be defined in `sonarcloud` repository environment + # # SONAR_ORGANIZATION: ${{ vars.SONAR_ORGANIZATION }} # SONAR_ORGANIZATION must be defined in `sonarcloud` repository environment + # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # SONAR_TOKEN must be defined in `sonarcloud` repository environment. SonarCloud access token should be generated from https://sonarcloud.io/account/security/ + + # # SonarCloud GitHub Action fails when a NPM project is detected and recommends usage of NPM Sonar plugin + # - name: SonarCloud Scan via NPM (pull_request) + # if: github.event_name == 'pull_request' + # # Get SONAR_ORGANIZATION and SONAR_PROJECT_KEY from https://sonarcloud.io/project/information?id=paul-gilber_demoapp-frontend + # # SONAR_ORGANIZATION must be defined in `sonarcloud` repository environment + # # SONAR_PROJECT_KEY must be defined in `sonarcloud` repository environment + # run: | + # mvn -B verify \ + # org.sonarsource.scanner.npm:sonar-npm-plugin:sonar \ + # -Drevision=${{ env.VERSION }} \ + # -Dsonar.organization=${{ vars.SONAR_ORGANIZATION }} \ + # -Dsonar.projectKey=${{ vars.SONAR_PROJECT_KEY }} \ + # -Dsonar.pullrequest.provider=GitHub \ + # -Dsonar.pullrequest.github.repository=${{ github.repository }} \ + # -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} \ + # -Dsonar.pullrequest.branch=${{ github.head_ref }} \ + # -Dsonar.pullrequest.base=${{ github.base_ref }} \ + # -Dmaven.test.skip=true \ + # -Ddockerfile.skip=true + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN is a special secret automatically generated by GitHub: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret + # SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} # SONAR_HOST_URL must be defined in `sonarcloud` repository environment + # # SONAR_ORGANIZATION: ${{ vars.SONAR_ORGANIZATION }} # SONAR_ORGANIZATION must be defined in `sonarcloud` repository environment + # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # SONAR_TOKEN must be defined in `sonarcloud` repository environment. SonarCloud access token should be generated from https://sonarcloud.io/account/security/ + + # In case you need to override default settings + # - name: SonarCloud Scan via NPM + # run: | + # mvn -B verify \ + # org.sonarsource.scanner.npm:sonar-npm-plugin:sonar \ + # -Dmaven.test.skip=true \ + # -Ddockerfile.skip=true \ + # -Dsonar.organization=my-organization \ + # -Dsonar.projectKey=my-projectkey \ + # -Dsonar.python.coverage.reportPaths=coverage.xml \ + # -Dsonar.sources=lib/ \ + # -Dsonar.test.exclusions=tests/** \ + # -Dsonar.tests=tests/ \ + # -Dsonar.verbose=true + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN is a special secret automatically generated by GitHub: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret + # SONAR_ORGANIZATION: ${{ vars.SONAR_ORGANIZATION }} # SONAR_ORGANIZATION must be defined in `sonarcloud` repository environment + # SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} # SONAR_HOST_URL must be defined in `sonarcloud` repository environment + # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # SONAR_TOKEN must be defined in `sonarcloud` repository environment. SonarCloud access token should be generated from https://sonarcloud.io/account/security/ diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..819c29a --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,25 @@ +--- +name: Pull Request Labeler + +# Events that trigger workflows: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +on: + pull_request: # By default, a workflow only runs when a pull_request event's activity type is opened, synchronize, or reopened + +# Set Workflow-level permissions: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: + contents: read + +jobs: + labeler: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 # https://github.com/marketplace/actions/checkout + + - uses: actions/labeler@v4 + with: + configuration-path: ./.github/labeler.yml + sync-labels: true # Whether or not to remove labels when matching files are reverted or no longer changed by the PR + dot: true # Whether or not to auto-include paths starting with dot (e.g. .github) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..5ce328f --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,39 @@ +--- +# Workflow syntax for GitHub Actions: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +name: Lint Files + +# Events: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +on: + # Run workflow on push except for ignored branches and paths + push: + branches-ignore: + - 'cherry-pick-*' + paths: + - '**.yaml' + - '**.yml' + - '**.yamllint' + # Run workflow on pull request + pull_request: # By default, a workflow only runs when a pull_request event's activity type is opened, synchronize, or reopened + + # Allow other Workflows to call this Workflow + workflow_call: + +# Run a single job at a time: https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Set Workflow-level permissions: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: + contents: read + +jobs: + lint: + # Run job when not triggered by a merge + if: (github.event_name == 'push' && contains(toJSON(github.event.head_commit.message), 'Merge pull request ') == false) || (github.event_name != 'push') + runs-on: ubuntu-latest # GitHub-hosted runners: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + steps: + - name: Checkout repository + uses: actions/checkout@v4 # https://github.com/marketplace/actions/checkout + - name: YAML Lint + run: yamllint . # yamllint is pre-installed: https://github.com/actions/runner-images/blob/main/images/linux/Ubuntu2204-Readme.md#tools diff --git a/.github/workflows/merge-cleanup.yml b/.github/workflows/merge-cleanup.yml new file mode 100644 index 0000000..9fc22d4 --- /dev/null +++ b/.github/workflows/merge-cleanup.yml @@ -0,0 +1,78 @@ +--- +# Workflow syntax for GitHub Actions: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +name: Merge Cleanup + +# Events: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +on: + # Run Workflow upon pull request + pull_request: + types: [closed] + + # Allow user to manually trigger Workflow execution + workflow_dispatch: + + # Run Workflow upon pull request to specified target branch(es) + # pull_request_target: + # types: [closed] + # branches: + # - main + +# Set Workflow-level permissions: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: {} # This Workflow does not require any permission + +# Set Workflow-level environment variables +env: + PROJECT: demoapp-frontend + +jobs: + output-information: + runs-on: ubuntu-latest + steps: + - name: Output Information + run: | + echo "${{ toJSON(github) }}" + + # Skopeo deletes image tag by reference which is not allowed by Docker Hub + # docker-hub-skopeo: + # runs-on: ubuntu-latest + # environment: docker-hub + # # Skopeo is pre-installed in GitHub hosted runners: https://github.com/actions/runner-images/blob/main/images/linux/Ubuntu2004-Readme.md#tools + # - name: Skopeo Login + # run: | + # skopeo login ${{ vars.DOCKER_REGISTRY_URL }} \ + # --username ${{ secrets.DOCKER_REGISTRY_USERNAME }} \ + # --password ${{ secrets.DOCKER_REGISTRY_PASSWORD }} \ + # - name: Skopeo Delete Image + # id: skopeo-delete + # run: | + # skopeo delete docker://${{ vars.DOCKER_REGISTRY_URL }}/${{ vars.DOCKER_REPOSITORY }}/${{ env.PROJECT }}:${{ env.VERSION }} + + # Regctl allows image tag deletion which is allowed by Docker Hub + docker-hub-regctl: + runs-on: ubuntu-latest + environment: docker-hub + steps: + # Workaround for the absence of github.branch_name + # Setting an environment variable: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable + - name: Set VERSION + if: github.head_ref != '' + run: | + echo "VERSION=${{ github.head_ref }}" >> $GITHUB_ENV + - name: Set VERSION + if: github.head_ref == '' + run: | + echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV + + # Install regctl: https://github.com/regclient/regclient + - name: Install regctl + run: | + curl -L https://github.com/regclient/regclient/releases/latest/download/regctl-linux-amd64 > regctl + chmod 755 regctl + - name: regctl login + run: | + ./regctl registry login ${{ vars.DOCKER_REGISTRY_URL }} \ + --user ${{ secrets.DOCKER_REGISTRY_USERNAME }} \ + --pass ${{ secrets.DOCKER_REGISTRY_PASSWORD }} \ + - name: regctl Delete Image Tag + run: | + ./regctl tag delete ${{ vars.DOCKER_REGISTRY_URL }}/${{ vars.DOCKER_REPOSITORY }}/${{ env.PROJECT }}:${{ env.VERSION }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..47d7e95 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,43 @@ +--- +# Release Drafter: https://github.com/marketplace/actions/release-drafter +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - main + # pull_request event is required only for autolabeler + pull_request: + # Only following types are handled by the action, but one can default to all as well + types: [opened, reopened, synchronize] + # pull_request_target event is required for autolabeler to support PRs from forks + # pull_request_target: + # types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + runs-on: ubuntu-latest + steps: + # (Optional) GitHub Enterprise requires GHE_HOST variable set + # - name: Set GHE_HOST + # run: | + # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV + + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + # with: + # config-name: my-config.yml + # disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2a75251 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,70 @@ +--- +# Publishing Docker images: https://docs.github.com/en/actions/publishing-packages/publishing-docker-images +# Automatically generated release notes https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes +name: Publish Container Image + +# Events: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +on: + # Trigger Workflow on push event + push: + branches: + - main # When `latest` image tag is published when default branch is update + tags: + - 'v*' # When a tag starting with `v` is created e.g. v1.0.0 + +# Set Workflow-level environment variables +env: + PROJECT: demoapp-frontend + +jobs: + push_to_registries: + name: Push Container image to multiple registries + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 # https://github.com/marketplace/actions/docker-setup-build + + - name: Log in to the Container registry + uses: docker/login-action@v3 # https://github.com/marketplace/actions/docker-login + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 # https://github.com/marketplace/actions/docker-metadata-action + with: + images: | + ghcr.io/${{ github.repository }} + tags: | + # set latest tag for default branch + type=raw,value=latest,enable={{is_default_branch}} + # branch event + type=ref,event=branch + # dynamically set the branch name and tag as a prefix (short sha format) + type=sha,prefix={{branch}}{{tag}}- + # tag event + type=ref,event=tag + + # Docker Buildx Bake supports multi-platform builds: https://docs.docker.com/build/bake/ + - name: Docker Build and Push (Bake) + env: + BUILD_IMAGE: registry.access.redhat.com/ubi8/nodejs-18 + RUNTIME_IMAGE: registry.access.redhat.com/ubi8/nodejs-18 + uses: docker/bake-action@v4 # https://github.com/marketplace/actions/docker-buildx-bake + with: + files: | + ./docker-bake.hcl + ${{ steps.meta.outputs.bake-file }} + set: | + build.args.BUILD_IMAGE=${{ env.BUILD_IMAGE }} + build.args.RUNTIME_IMAGE=${{ env.RUNTIME_IMAGE }} + targets: gha-build + push: true diff --git a/.gitignore b/.gitignore index 4d29575..8a8b5da 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# env.sh +public/env-config.js diff --git a/.yamllint b/.yamllint index 11e6a66..8383e4c 100644 --- a/.yamllint +++ b/.yamllint @@ -4,6 +4,9 @@ yaml-files: - '*.yml' - '.yamllint' +ignore: + - node_modules/ + rules: anchors: enable braces: enable diff --git a/Containerfile b/Containerfile index 41a0383..9bff5b3 100644 --- a/Containerfile +++ b/Containerfile @@ -3,13 +3,13 @@ ARG RUNTIME_IMAGE="registry.access.redhat.com/ubi8/nodejs-18" # Build FROM ${BUILD_IMAGE} as build -WORKDIR /app +WORKDIR /build -COPY --chown=default:default package.json ./package.json -COPY --chown=default:default public ./public -COPY --chown=default:default src ./src +COPY package.json ./package.json +COPY public ./public +COPY src ./src -RUN npm install --silent \ +RUN npm install \ && npm run build @@ -17,7 +17,11 @@ RUN npm install --silent \ FROM ${RUNTIME_IMAGE} WORKDIR /app -COPY --from=build --chown=default:default /app/build ./build +COPY --from=build --chown=default:default /build/build /app/build +# .env file is copied during runtime and will be appended to env-config.js +COPY --chown=1001:0 --chmod=644 .env /app/.env +# env.sh is used for generating env-config.js, env.sh will override .env entries when they are defined in the environment variables +COPY --chown=1001:0 --chmod=755 env.sh /app/env.sh RUN npm install -g serve -CMD serve -n -s build -l tcp://0.0.0.0:8080 +CMD /app/env.sh build && serve -n -s /app/build -l tcp://0.0.0.0:8080 diff --git a/README.md b/README.md index f32cd32..bdc8214 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,5 @@ docker build -f Containerfile -t demoapp-frontend . docker run -p 8080:8080 demoapp-frontend ``` -## Changes -1. Setup of [Visual Studio Code](https://code.visualstudio.com/) [Dev Container](https://code.visualstudio.com/docs/devcontainers/containers). See [devcontainer.json](./.devcontainer/devcontainer.json) -2. Replacement of `localhost` hostname for backend with `demoapp-backend` -3. Creation of [Containerfile](./Containerfile) +## References +- [Deploying ReactJS to multiple environments](https://adjoe.io/engineer-blog/react-applications-build-once-deploy-anywhere-in-react/) diff --git a/container-structure-test.yaml b/container-structure-test.yaml new file mode 100644 index 0000000..336fd5d --- /dev/null +++ b/container-structure-test.yaml @@ -0,0 +1,65 @@ +--- +# Container Structure Tests: https://github.com/GoogleContainerTools/container-structure-test +# Run command: +# > container-structure-test test --image demoapp-frontend --config container-structure-test.yaml +schemaVersion: 2.0.0 + +metadataTest: + envVars: + # Confirm NodeJS version + - key: NODEJS_VERSION + value: '18' + # Confirm Image OCI compliance + - key: container + value: oci + labels: + # Confirm labels from Red Hat provided base image + - key: com.redhat.component + value: nodejs-18-container # Confirm base image was provided by Red Hat + - key: vendor + value: Red Hat, Inc. + exposedPorts: ["8080"] + volumes: [] + entrypoint: ["container-entrypoint"] + cmd: [ + "/bin/sh", + "-c", + "/app/env.sh build && serve -n -s /app/build -l tcp://0.0.0.0:8080" + ] + workdir: /app + user: 1001 + +commandTests: + - name: Confirm Node JS Version + command: node + args: ['-v'] + expectedOutput: ['v18.*'] # Version output is sent to stdout + +fileExistenceTests: + - name: Confirm absence of application source files + path: /app/src + shouldExist: false + - name: Confirm absence of application source files + path: /app/public + shouldExist: false + - name: Confirm existence of /app/.env + path: /app/.env + shouldExist: true + permissions: -rw-r--r-- + # UID and GID values are from based image: registry.access.redhat.com/ubi8/nodejs-18 + uid: 1001 + gid: 0 + - name: Confirm existence of /app/env.sh + path: /app/env.sh + shouldExist: true + permissions: -rwxr-xr-x + # UID and GID values are from based image: registry.access.redhat.com/ubi8/nodejs-18 + uid: 1001 + gid: 0 + - name: Confirm existence of application build + path: /app/build + shouldExist: true + permissions: drwxr-xr-x + # UID and GID values are from based image: registry.access.redhat.com/ubi8/nodejs-18 + uid: 1001 + gid: 0 diff --git a/.devcontainer/.env b/deploy/docker-compose/.env similarity index 61% rename from .devcontainer/.env rename to deploy/docker-compose/.env index 55be651..68150fb 100644 --- a/.devcontainer/.env +++ b/deploy/docker-compose/.env @@ -1,6 +1,8 @@ +DEMOAPP_FRONTEND_IMAGE="demoapp-frontend:latest" # Locally built image DEMOAPP_BACKEND_IMAGE="ghcr.io/paul-gilber/demoapp-backend:latest" # https://github.com/paul-gilber/demoapp-backend/pkgs/container/demoapp-backend/143156481?tag=latest +DEMOAPP_BACKEND_URL="http://localhost:8080" # Set demoapp-backend url for local testing MYSQL_IMAGE="mysql:8.0" # https://hub.docker.com/_/mysql MYSQL_ROOT_PASSWORD="local" MYSQL_DATABASE="demoapp" MYSQL_USER="user" -MYSQL_PASSWORD="password" \ No newline at end of file +MYSQL_PASSWORD="password" diff --git a/deploy/docker-compose/compose.yaml b/deploy/docker-compose/compose.yaml new file mode 100644 index 0000000..201603e --- /dev/null +++ b/deploy/docker-compose/compose.yaml @@ -0,0 +1,102 @@ +--- +# Networks top-level element reference: https://docs.docker.com/compose/compose-file/06-networks/ +networks: + backend: + # driver: host + frontend: + # driver: host + +# Service top-level element reference: https://docs.docker.com/compose/compose-file/05-services/ +services: + nginx: + depends_on: + demoapp-frontend: + condition: service_healthy # healthy status is indicated by `healthcheck` keyword + build: + context: ../../nginx + dockerfile: Containerfile + ports: + - "8080:80" # Forwards container port 80 to host port 8080. URL: http://localhost:8080/ + networks: + - backend # Connect to `backend` network + - frontend # Connect to `frontend` network + demoapp-frontend: + depends_on: + demoapp-backend: + condition: service_healthy # healthy status is indicated by `healthcheck` keyword + # image: ${DEMOAPP_FRONTEND_IMAGE} # use image specified in .env file + # Build image from Containerfile + build: + context: ../../ + dockerfile: Containerfile + environment: + REACT_APP_DEMOAPP_BACKEND_URL: "${DEMOAPP_BACKEND_URL}" # Override default demoapp-backend url + healthcheck: + # Check Spring Boot Actuator + test: curl --fail http://localhost:8080/ # command for testing health + # Specifying durations: https://docs.docker.com/compose/compose-file/11-extension/#specifying-durations + interval: 5s # specifies the time duration or interval in which the healthcheck process will execute + timeout: 1s # defines the time duration to wait for a healthcheck + retries: 10 # is used to define the number of tries to implement the health check after failure + # Red Hat runtime image already exposes port 8080, thus `expose` keyword can be omitted + # expose: + # - "8080" + restart: always + networks: + - backend # Connect to `backend` network + - frontend # Connect to `frontend` network + demoapp-backend: + depends_on: + mysql: + condition: service_healthy # healthy status is indicated by `healthcheck` keyword + image: ${DEMOAPP_BACKEND_IMAGE} # use image specified in .env file + # Build image from Containerfile + # build: + # context: . + # dockerfile: Containerfile.multistage + environment: + # Externalized Spring Configuration: https://docs.spring.io/spring-boot/docs/1.5.6.RELEASE/reference/html/boot-features-external-config.html + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD} + healthcheck: + # Check Spring Boot Actuator + test: curl --fail http://localhost:8080/actuator/health # command for testing health + # Specifying durations: https://docs.docker.com/compose/compose-file/11-extension/#specifying-durations + interval: 5s # specifies the time duration or interval in which the healthcheck process will execute + timeout: 1s # defines the time duration to wait for a healthcheck + retries: 10 # is used to define the number of tries to implement the health check after failure + # Red Hat runtime image already exposes port 8080, thus `expose` keyword can be omitted + # expose: + # - "8080" + restart: always + networks: + - backend # Connect to `backend` network + - frontend # Connect to `frontend` network + mysql: # Service name is also used as hostname when connecting from other containers + image: ${MYSQL_IMAGE} # use image specified in .env file + # Docker Hub mysql image already exposes port 3306, thus `expose` keyword can be omitted + # expose: + # - "3306" + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + healthcheck: + # Login to mysql demoapp db + test: mysql --host=localhost --user=root --password=$$MYSQL_ROOT_PASSWORD demoapp # command for testing health + # Specifying durations: https://docs.docker.com/compose/compose-file/11-extension/#specifying-durations + interval: 5s # specifies the time duration or interval in which the healthcheck process will execute + timeout: 1s # defines the time duration to wait for a healthcheck + retries: 10 # is used to define the number of tries to implement the health check after failure + restart: always + volumes: + - type: volume + source: db-data + target: /var/lib/mysql + networks: + - backend # Connect to `backend` network + +# Volumes top-level element reference: https://docs.docker.com/compose/compose-file/07-volumes/ +volumes: + db-data: {} diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..cad772c --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,42 @@ +// Configuration for docker/bake-action: https://github.com/docker/bake-action +// https://github.com/marketplace/actions/docker-metadata-action#bake-definition +// docker-bake.hcl + +target "docker-metadata-action" {} + +// Create Base Build Target +target "build" { + inherits = ["docker-metadata-action"] + context = "./" + // Use multi-stage Containerfile + dockerfile = "Containerfile" +} + +// Create Platforms Target +target "platforms" { + // Set target platforms for multi-platform builds https://docs.docker.com/build/bake/reference/#targetplatforms + platforms = [ + "linux/amd64", + "linux/arm64" + ] +} + +// Create Multi Platform Build Target +target "multi-platform-build" { + inherits = ["build", "platforms"] +} + +// Create GitHub Action Cache Target +target "gha-cache" { + cache-from = [ + "type=gha" + ] + cache-to = [ + "type=gha,mode=max" + ] +} + +// Create GitHub Action Build Target +target "gha-build" { + inherits = ["multi-platform-build", "gha-cache"] +} diff --git a/env.sh b/env.sh new file mode 100755 index 0000000..ff4b4ba --- /dev/null +++ b/env.sh @@ -0,0 +1,27 @@ +#!/bin/bash +output_dir="$1" + +output_file_name="env-config.js" + +if [ -z "${output_dir}" ]; then + output_dir="./public" +fi + +if [ -f "${output_dir}/${output_file_name}" ]; then + rm -f "${output_dir}/${output_file_name}" +fi + +echo "window.env = {" >> "${output_dir}/${output_file_name}" + +while read -r line || [[ -n "$line" ]]; +do + if printf '%s\n' "$line" | grep -q -e '='; then + varname=$(printf '%s\n' "$line" | sed -e 's/=.*//') + varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//') + fi + value=$(printf '%s\n' "${!varname}") + [[ -z $value ]] && value=${varvalue} + echo " $varname: \"$value\"," >> "${output_dir}/${output_file_name}" +done < <(grep -v "^#" .env) + +echo "}" >> "${output_dir}/${output_file_name}" \ No newline at end of file diff --git a/nginx/Containerfile b/nginx/Containerfile new file mode 100644 index 0000000..39b2286 --- /dev/null +++ b/nginx/Containerfile @@ -0,0 +1,4 @@ +# docker build -f Containerfile . + +FROM nginx:latest +COPY default.conf /etc/nginx/conf.d/default.conf diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..832c30f --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,61 @@ +upstream backend { + server demoapp-backend:8080; +} + +upstream frontend { + server demoapp-frontend:8080; +} + +server { + listen 80; + listen [::]:80; + server_name demoapp.example.com; + + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Nginx-Proxy true; + proxy_pass http://frontend/; + proxy_set_header Host $http_host; + proxy_pass_header Server; + proxy_redirect off; + } + + location /users { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Nginx-Proxy true; + proxy_pass http://backend/users; + proxy_set_header Host $http_host; + proxy_pass_header Server; + proxy_redirect off; + } + + location /user { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Nginx-Proxy true; + proxy_pass http://backend/user; + proxy_set_header Host $http_host; + proxy_pass_header Server; + proxy_redirect off; + } + + # Sample rule if you want a common prefix for the backend + # location /api { + # rewrite ^/api/(.*) /$1 break; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Nginx-Proxy true; + # proxy_pass http://backend/; + # proxy_set_header Host $http_host; + # proxy_pass_header Server; + # proxy_redirect off; + # } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ef9ecd9..a0bb2a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^0.27.2", "bootstrap": "^5.1.3", + "http": "^0.0.1-security", "react": "^18.1.0", "react-dom": "^18.1.0", "react-router-dom": "^6.3.0", @@ -8281,6 +8282,11 @@ "entities": "^2.0.0" } }, + "node_modules/http": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", + "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" + }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -22245,6 +22251,11 @@ "entities": "^2.0.0" } }, + "http": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", + "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" + }, "http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", diff --git a/package.json b/package.json index ae4e28b..4648b1d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^0.27.2", "bootstrap": "^5.1.3", + "http": "^0.0.1-security", "react": "^18.1.0", "react-dom": "^18.1.0", "react-router-dom": "^6.3.0", @@ -15,6 +16,9 @@ "web-vitals": "^2.1.4" }, "scripts": { + "compose:up": "docker compose --project-directory deploy/docker-compose up -d", + "compose:stop": "docker compose --project-directory deploy/docker-compose stop", + "compose:down": "docker compose --project-directory deploy/docker-compose down", "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", diff --git a/public/index.html b/public/index.html index aa069f2..392c0e6 100644 --- a/public/index.html +++ b/public/index.html @@ -25,6 +25,7 @@ Learn how to configure a non-root public URL by running `npm run build`. --> React App + diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..b6ff206 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,3 @@ +const DEMOAPP_BACKEND_URL = window.env.REACT_APP_DEMOAPP_BACKEND_URL; + +exports.DEMOAPP_BACKEND_URL = DEMOAPP_BACKEND_URL \ No newline at end of file diff --git a/src/index.js b/src/index.js index d563c0f..17d0e53 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; + const root = ReactDOM.createRoot(document.getElementById('root')); root.render( diff --git a/src/pages/Home.js b/src/pages/Home.js index aa83880..06511b1 100644 --- a/src/pages/Home.js +++ b/src/pages/Home.js @@ -1,24 +1,32 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; -import { Link, useParams } from "react-router-dom"; +import { Link } from "react-router-dom"; +const { DEMOAPP_BACKEND_URL } = require('../constants'); export default function Home() { const [users, setUsers] = useState([]); // eslint-disable-next-line - const { id } = useParams(); useEffect(() => { loadUsers(); }, []); const loadUsers = async () => { - const result = await axios.get("http://demoapp-backend:8080/users"); // Updated by Paul Gilber + const result = await axios.get( + `${DEMOAPP_BACKEND_URL}/users`, + { + headers: { + "Access-Control-Allow-Origin": `${DEMOAPP_BACKEND_URL}`, + "Access-Control-Allow-Headers": 'Content-Type, Authorization' + } + } + ); // Updated by Paul Gilber setUsers(result.data); }; const deleteUser = async (id) => { - await axios.delete(`http://demoapp-backend:8080/user/${id}`); // Updated by Paul Gilber + await axios.delete(`${DEMOAPP_BACKEND_URL}/user/${id}`); // Updated by Paul Gilber loadUsers(); }; @@ -36,10 +44,10 @@ export default function Home() { - {users.map((user, index) => ( - - - {index + 1} + {users.map((user) => ( + + + {user.id} {user.name} {user.username} diff --git a/src/users/AddUser.js b/src/users/AddUser.js index 4be5351..8d6e25b 100644 --- a/src/users/AddUser.js +++ b/src/users/AddUser.js @@ -1,6 +1,7 @@ import axios from "axios"; import React, { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; +const { DEMOAPP_BACKEND_URL } = require('../constants'); export default function AddUser() { let navigate = useNavigate(); @@ -19,7 +20,7 @@ export default function AddUser() { const onSubmit = async (e) => { e.preventDefault(); - await axios.post("http://demoapp-backend:8080/user", user); // Updated by Paul Gilber + await axios.post(`${DEMOAPP_BACKEND_URL}/user`, user); // Updated by Paul Gilber navigate("/"); }; diff --git a/src/users/EditUser.js b/src/users/EditUser.js index 182d9fa..a94e0e8 100644 --- a/src/users/EditUser.js +++ b/src/users/EditUser.js @@ -1,6 +1,7 @@ import axios from "axios"; import React, { useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; +const { DEMOAPP_BACKEND_URL } = require('../constants'); export default function EditUser() { let navigate = useNavigate(); @@ -26,12 +27,12 @@ export default function EditUser() { const onSubmit = async (e) => { e.preventDefault(); - await axios.put(`http://demoapp-backend:8080/user/${id}`, user); // Updated by Paul Gilber + await axios.put(`${DEMOAPP_BACKEND_URL}/user/${id}`, user); // Updated by Paul Gilber navigate("/"); }; const loadUser = async () => { - const result = await axios.get(`http://demoapp-backend:8080/user/${id}`); // Updated by Paul Gilber + const result = await axios.get(`${DEMOAPP_BACKEND_URL}/user/${id}`); // Updated by Paul Gilber setUser(result.data); }; diff --git a/src/users/ViewUser.js b/src/users/ViewUser.js index 8d4e995..91254a3 100644 --- a/src/users/ViewUser.js +++ b/src/users/ViewUser.js @@ -1,6 +1,7 @@ import axios from "axios"; import React, { useEffect,useState } from "react"; import { Link, useParams } from "react-router-dom"; +const { DEMOAPP_BACKEND_URL } = require('../constants'); export default function ViewUser() { const [user, setUser] = useState({ @@ -17,7 +18,7 @@ export default function ViewUser() { }, []); const loadUser = async () => { - const result = await axios.get(`http://demoapp-backend:8080/user/${id}`); // Updated by Paul Gilber + const result = await axios.get(`${DEMOAPP_BACKEND_URL}/user/${id}`); // Updated by Paul Gilber setUser(result.data); };