From e679b741a2297e9fde38b2f5f57fb2e02d94792a Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Mon, 6 Nov 2023 11:43:29 -0300 Subject: [PATCH] Feat: add database (#129) * fix: merge main workflow name * feat: add postgres blank database jira task 221 * test: add application properties for test * ci: add files and workflows for databse deploy jira issue 221 * ci: fix workflow parameters * ci: fix matrix name vars --- .github/workflows/merge.yml | 62 +++++- .github/workflows/pr-open.yml | 44 ++++- backend/openshift.deploy.yml | 43 +++- backend/pom.xml | 10 + .../src/main/resources/application.properties | 19 ++ backend/src/main/resources/data.sql | 11 ++ .../src/test/resources/application.properties | 13 ++ common/openshift.init.yml | 55 ++++++ database/Dockerfile | 10 + database/openshift.deploy.yml | 185 ++++++++++++++++++ docker-compose.yml | 24 +++ 11 files changed, 456 insertions(+), 20 deletions(-) create mode 100644 backend/src/main/resources/data.sql create mode 100644 backend/src/test/resources/application.properties create mode 100644 common/openshift.init.yml create mode 100644 database/Dockerfile create mode 100644 database/openshift.deploy.yml diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index eaec9505..af854d78 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -34,21 +34,43 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 + init-test: + name: TEST Init + environment: test + runs-on: ubuntu-22.04 + steps: + - name: OpenShift Init + uses: bcgov-nr/action-deployer-openshift@v1.0.4 + with: + oc_namespace: ${{ vars.OC_NAMESPACE }} + oc_server: ${{ vars.OC_SERVER }} + oc_token: ${{ secrets.OC_TOKEN }} + file: common/openshift.init.yml + overwrite: false + parameters: + -p ZONE=test -p NAME=${{ github.event.repository.name }} + deploys-test: name: TEST Deployments + needs: [init-test] environment: test runs-on: ubuntu-22.04 permissions: issues: write strategy: matrix: - name: [backend, frontend] + name: [database, backend, frontend] include: + - name: database - name: backend + parameters: + -p PROMOTE=${{ github.repository }}/backend:test + verification_path: "actuator/health" - name: frontend parameters: -p VITE_USER_POOLS_ID=${{ vars.VITE_USER_POOLS_ID }} -p VITE_USER_POOLS_WEB_CLIENT_ID=${{ vars.VITE_USER_POOLS_WEB_CLIENT_ID }} + -p PROMOTE=${{ github.repository }}/frontend:test steps: - uses: bcgov-nr/action-deployer-openshift@v1.4.0 with: @@ -58,7 +80,7 @@ jobs: oc_token: ${{ secrets.OC_TOKEN }} overwrite: true parameters: - -p ZONE=test -p PROMOTE=${{ github.repository }}/${{ matrix.name }}:test + -p ZONE=test -p NAME=${{ github.event.repository.name }} ${{ matrix.parameters }} penetration_test: true @@ -66,21 +88,43 @@ jobs: penetration_test_issue: ${{ matrix.name }} penetration_test_token: ${{ secrets.GITHUB_SECRET }} + init-prod: + name: PROD Init + needs: [deploys-test] + environment: prod + runs-on: ubuntu-22.04 + steps: + - name: OpenShift Init + uses: bcgov-nr/action-deployer-openshift@v1.0.4 + with: + oc_namespace: ${{ vars.OC_NAMESPACE }} + oc_server: ${{ vars.OC_SERVER }} + oc_token: ${{ secrets.OC_TOKEN }} + file: common/openshift.init.yml + overwrite: false + parameters: + -p ZONE=prod -p NAME=${{ github.event.repository.name }} + deploys-prod: name: PROD Deployments - needs: [deploys-test] + needs: [init-prod] environment: prod runs-on: ubuntu-22.04 strategy: matrix: - name: [backend, frontend] + name: [database, backend, frontend] include: + - name: database - name: backend + parameters: + -p PROMOTE=${{ github.repository }}/backend:test + - name: frontend + parameters: + -p PROMOTE=${{ github.repository }}/frontend:test paratemeters: -p VITE_USER_POOLS_ID=${{ vars.VITE_USER_POOLS_ID }} -p VITE_USER_POOLS_WEB_CLIENT_ID=${{ vars.VITE_USER_POOLS_WEB_CLIENT_ID }} -p VITE_REDIRECT_SIGN_OUT="${{ vars.VITE_REDIRECT_SIGN_OUT }}" - - name: frontend steps: - uses: bcgov-nr/action-deployer-openshift@v1.4.0 with: @@ -90,8 +134,9 @@ jobs: oc_token: ${{ secrets.OC_TOKEN }} overwrite: true parameters: - -p ZONE=prod -p PROMOTE=${{ github.repository }}/${{ matrix.name }}:test + -p ZONE=prod -p NAME=${{ github.event.repository.name }} + ${{ matrix.parameters }} image-promotions: name: Promote images to PROD @@ -99,10 +144,13 @@ jobs: runs-on: ubuntu-22.04 permissions: packages: write + strategy: + matrix: + component: [database, backend, frontend] steps: - uses: shrink/actions-docker-registry-tag@v3 with: registry: ghcr.io - repository: ${{ github.repository }}/frontend + repository: ${{ github.repository }}/${{ matrix.component }} target: test tags: prod diff --git a/.github/workflows/pr-open.yml b/.github/workflows/pr-open.yml index 5a1c59e2..8cf56d41 100644 --- a/.github/workflows/pr-open.yml +++ b/.github/workflows/pr-open.yml @@ -37,16 +37,32 @@ jobs: Once merged, code will be promoted and handed off to following workflow run. [Main Merge Workflow](https://github.com/${{ github.repository }}/actions/workflows/merge-main.yml) + - name: OpenShift Init + uses: bcgov-nr/action-deployer-openshift@v1.1.1 + with: + oc_namespace: ${{ vars.OC_NAMESPACE }} + oc_server: ${{ vars.OC_SERVER }} + oc_token: ${{ secrets.OC_TOKEN }} + file: common/openshift.init.yml + overwrite: false + parameters: + -p ZONE=${{ github.event.number }} + -p NAME=${{ github.event.repository.name }} + triggers: ('common/' 'database/' 'backend/' 'frontend/') + builds: name: Builds + needs: [pr-greeting] if: "!github.event.pull_request.head.repo.fork" runs-on: ubuntu-22.04 permissions: packages: write strategy: matrix: - name: [backend, frontend] + name: [database, backend, frontend] include: + - package: database + triggers: ('database/') - name: backend triggers: ('backend/') - name: frontend @@ -64,22 +80,35 @@ jobs: deploys: name: Deploys if: "!github.event.pull_request.head.repo.fork" - needs: [builds] + needs: [builds, pr-greeting] runs-on: ubuntu-22.04 strategy: matrix: - name: [backend, frontend] + name: [database, backend, frontend] include: + - name: database + file: database/openshift.deploy.yml + parameters: + -p DB_PVC_SIZE=128Mi + overwrite: false + triggers: ('common/' 'database/' 'backend/' 'frontend/') - name: backend file: backend/openshift.deploy.yml - triggers: ('backend/' 'frontend/') + triggers: ('common/' 'database/' 'backend/' 'frontend/') verification_path: /actuator/health + parameters: + -p PROMOTE=${{ github.repository }}/backend:${{ github.event.number }} + -p MIN_REPLICAS=1 + -p MAX_REPLICAS=2 - name: frontend file: frontend/openshift.deploy.yml - triggers: ('backend/' 'frontend/') + triggers: ('common/' 'database/' 'backend/' 'frontend/') parameters: -p VITE_USER_POOLS_ID=${{ vars.VITE_USER_POOLS_ID }} -p VITE_USER_POOLS_WEB_CLIENT_ID=${{ vars.VITE_USER_POOLS_WEB_CLIENT_ID }} + -p PROMOTE=${{ github.repository }}/frontend:${{ github.event.number }} + -p MIN_REPLICAS=1 + -p MAX_REPLICAS=2 steps: - uses: bcgov-nr/action-deployer-openshift@v1.4.0 with: @@ -89,9 +118,8 @@ jobs: oc_token: ${{ secrets.OC_TOKEN }} overwrite: true parameters: - -p ZONE=${{ github.event.number }} -p NAME=${{ github.event.repository.name }} - -p PROMOTE=${{ github.repository }}/${{ matrix.name }}:${{ github.event.number }} - -p MIN_REPLICAS=1 -p MAX_REPLICAS=2 + -p ZONE=${{ github.event.number }} + -p NAME=${{ github.event.repository.name }} ${{ matrix.parameters }} triggers: ${{ matrix.triggers }} verification_path: ${{ matrix.verification_path }} diff --git a/backend/openshift.deploy.yml b/backend/openshift.deploy.yml index 422a929d..16df2ef1 100644 --- a/backend/openshift.deploy.yml +++ b/backend/openshift.deploy.yml @@ -3,7 +3,7 @@ kind: Template parameters: - name: NAME description: Module name - value: quickstart-openshift-backends + value: nr-silva - name: COMPONENT description: Component name value: backend @@ -38,6 +38,15 @@ parameters: - name: PROMOTE description: Image (namespace/name:tag) to promote/import value: bcgov/quickstart-openshift-backends/backend:test + - name: DB_POOL_CONN_TIMEOUT + description: Maximum number of milliseconds that a client will wait for a connection from the pool. + value: "90000" + - name: DB_POOL_IDLE_TIMEOUT + description: Maximum amount of milliseconds that a connection is allowed to sit idle in the pool. + value: "0" + - name: DB_POOL_MAX_LIFETIME + description: Maximum lifetime of a connection in the pool. + value: "1800000" objects: - apiVersion: v1 kind: ImageStream @@ -87,6 +96,30 @@ objects: - image: "${NAME}-${ZONE}-${COMPONENT}:${IMAGE_TAG}" imagePullPolicy: Always name: "${NAME}" + env: + - name: POSTGRES_HOST + value: ${NAME}-${ZONE}-database + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: ${NAME}-${ZONE}-database + key: database-name + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: ${NAME}-${ZONE}-database + key: database-password + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: ${NAME}-${ZONE}-database + key: database-user + - name: DB_POOL_CONN_TIMEOUT + value: ${DB_POOL_CONN_TIMEOUT} + - name: DB_POOL_IDLE_TIMEOUT + value: ${DB_POOL_IDLE_TIMEOUT} + - name: DB_POOL_MAX_LIFETIME + value: ${DB_POOL_MAX_LIFETIME} ports: - containerPort: 8080 protocol: TCP @@ -103,13 +136,13 @@ objects: port: 8080 scheme: HTTP initialDelaySeconds: 5 - periodSeconds: 2 - timeoutSeconds: 2 + periodSeconds: 5 + timeoutSeconds: 10 successThreshold: 1 - failureThreshold: 30 + failureThreshold: 5 livenessProbe: successThreshold: 1 - failureThreshold: 3 + failureThreshold: 5 httpGet: path: /actuator/health port: 8080 diff --git a/backend/pom.xml b/backend/pom.xml index 8fe1145a..cb32f108 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -141,6 +141,16 @@ spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + + org.springframework.boot diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 5c7c4e1d..e106571a 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -12,3 +12,22 @@ springdoc.enable-native-support = true # https://github.com/quarkusio/quarkus/wiki/Migration-Guide-3.2 quarkus.native.additional-build-args=-march=x86-64-v2 + +# Database, and JPA +spring.datasource.driver-class-name = org.postgresql.Driver +spring.datasource.url = jdbc:postgresql://${POSTGRES_HOST:localhost}:5432/${POSTGRES_DB:postgres} +spring.datasource.username = ${POSTGRES_USER:postgres} +spring.datasource.password = ${POSTGRES_PASSWORD:default} +spring.datasource.hikari.connectionTimeout = ${DB_POOL_CONN_TIMEOUT:90000} +spring.datasource.hikari.idleTimeout = ${DB_POOL_IDLE_TIMEOUT:45000} +spring.datasource.hikari.maxLifetime = ${DB_POOL_MAX_LIFETIME:60000} +spring.datasource.hikari.keepaliveTime = 30000 +spring.datasource.hikari.poolName = NrSparDbPool +spring.datasource.hikari.minimumIdle = 1 +spring.datasource.hikari.maximumPoolSize = 3 +spring.jpa.properties.hibernate.default_schema = silva +spring.jpa.database-platform = org.hibernate.dialect.PostgreSQLDialect +spring.jpa.show-sql = true +spring.jpa.hibernate.ddl-auto = update +spring.jpa.defer-datasource-initialization=true +spring.sql.init.mode=always diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql new file mode 100644 index 00000000..7bb9f461 --- /dev/null +++ b/backend/src/main/resources/data.sql @@ -0,0 +1,11 @@ +CREATE SCHEMA IF NOT EXISTS silva; + +-- Example +CREATE TABLE IF NOT EXISTS silva.person ( + id INT NOT NULL, + name VARCHAR(30) NOT NULL, + CONSTRAINT person_pk + PRIMARY KEY(id) +); + +DROP TABLE silva.person; diff --git a/backend/src/test/resources/application.properties b/backend/src/test/resources/application.properties new file mode 100644 index 00000000..b4c6c298 --- /dev/null +++ b/backend/src/test/resources/application.properties @@ -0,0 +1,13 @@ +# Server and application +spring.profiles.active = dev + +# Database, datasource and JPA +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:silvadb;DB_CLOSE_DELAY=-1;NON_KEYWORDS=USER;DB_CLOSE_ON_EXIT=true +spring.datasource.username=result +spring.datasource.password=password +spring.datasource.initialization-mode=always +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.show-sql=true +spring.jpa.hibernate.generate-ddl = true +spring.jpa.hibernate.ddl-auto = create-drop diff --git a/common/openshift.init.yml b/common/openshift.init.yml new file mode 100644 index 00000000..7a12b910 --- /dev/null +++ b/common/openshift.init.yml @@ -0,0 +1,55 @@ +apiVersion: template.openshift.io/v1 +kind: Template +parameters: + - name: NAME + description: Product name + value: nr-silva + - name: ZONE + description: Deployment zone, e.g. pr-### or prod + required: true + - name: PG_DATABASE + description: Postgres database name + value: database + - name: DB_PASSWORD + description: Password for the PostgreSQL connection user. + from: "[a-zA-Z0-9]{16}" + generate: expression +objects: + - apiVersion: v1 + kind: Secret + metadata: + name: ${NAME}-${ZONE}-${PG_DATABASE} + labels: + app: ${NAME}-${ZONE} + stringData: + database-name: ${NAME} + database-password: ${DB_PASSWORD} + database-user: ${NAME} + - apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-from-openshift-ingress + labels: + template: openshift-test + spec: + podSelector: {} + ingress: + - from: + - namespaceSelector: + matchLabels: + network.openshift.io/policy-group: ingress + policyTypes: + - Ingress + - apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-same-namespace + labels: + template: nr-spar-backend-network-security-policy + spec: + podSelector: {} + ingress: + - from: + - podSelector: {} + policyTypes: + - Ingress \ No newline at end of file diff --git a/database/Dockerfile b/database/Dockerfile new file mode 100644 index 00000000..4c219eb1 --- /dev/null +++ b/database/Dockerfile @@ -0,0 +1,10 @@ +FROM postgis/postgis:15-master + +# Enable pgcrypto extension on startup +RUN sed -i '/EXISTS postgis_tiger_geocoder;*/a CREATE EXTENSION IF NOT EXISTS pgcrypto;' \ + /docker-entrypoint-initdb.d/10_postgis.sh + +# User, port and Healthcheck +USER postgres +EXPOSE 5432 +HEALTHCHECK --interval=5s --timeout=5s --retries=5 CMD [ "pg_isready", "-U", "postgres"] diff --git a/database/openshift.deploy.yml b/database/openshift.deploy.yml new file mode 100644 index 00000000..4d05aaba --- /dev/null +++ b/database/openshift.deploy.yml @@ -0,0 +1,185 @@ +apiVersion: template.openshift.io/v1 +kind: Template +labels: + app: ${NAME}-${ZONE} +parameters: + - name: NAME + description: Product name + value: nr-silva + - name: COMPONENT + description: Component name + value: database + - name: ZONE + description: Deployment zone, e.g. pr-### or prod + required: true + - name: REGISTRY + description: Container registry to import from (internal is image-registry.openshift-image-registry.svc:5000) + value: ghcr.io + - name: ORG_NAME + description: Organization name + value: bcgov + - name: IMAGE_TAG + description: Image tag to use + value: latest + - name: PVC_MOUNT_PATH + description: Where to mount the PVC, subpath (e.g. data/) + value: /var/lib/postgresql + - name: CPU_REQUEST + value: 50m + - name: CPU_LIMIT + value: 115m + - name: MEMORY_REQUEST + value: 150Mi + - name: MEMORY_LIMIT + value: 250Mi + - name: DB_PVC_SIZE + description: Volume space available for data, e.g. 512Mi, 2Gi. + displayName: Database Volume Capacity + required: true + value: 256Mi +objects: + - kind: PersistentVolumeClaim + apiVersion: v1 + metadata: + name: ${NAME}-${ZONE}-${COMPONENT} + labels: + app: ${NAME}-${ZONE} + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: ${DB_PVC_SIZE} + storageClassName: netapp-file-standard + - kind: ImageStream + apiVersion: v1 + metadata: + name: ${NAME}-${ZONE}-${COMPONENT} + labels: + app: ${NAME}-${ZONE} + spec: + lookupPolicy: + local: false + tags: + - name: ${IMAGE_TAG} + from: + kind: DockerImage + name: ${REGISTRY}/${ORG_NAME}/${NAME}/${COMPONENT}:${ZONE} + referencePolicy: + type: Local + - kind: DeploymentConfig + apiVersion: v1 + metadata: + name: ${NAME}-${ZONE}-${COMPONENT} + labels: + app: ${NAME}-${ZONE} + spec: + replicas: 1 + triggers: + - type: ConfigChange + - type: ImageChange + imageChangeParams: + automatic: true + containerNames: + - ${NAME} + from: + kind: ImageStreamTag + name: ${NAME}-${ZONE}-${COMPONENT}:${IMAGE_TAG} + selector: + deploymentconfig: ${NAME}-${ZONE}-${COMPONENT} + strategy: + type: Recreate + recreateParams: + timeoutSeconds: 600 + activeDeadlineSeconds: 21600 + template: + metadata: + name: ${NAME}-${ZONE}-${COMPONENT} + labels: + app: ${NAME}-${ZONE} + deploymentconfig: ${NAME}-${ZONE}-${COMPONENT} + spec: + volumes: + - name: ${NAME}-${ZONE}-${COMPONENT} + persistentVolumeClaim: + claimName: ${NAME}-${ZONE}-${COMPONENT} + containers: + - name: ${NAME} + image: ${NAME}-${ZONE}-${COMPONENT}:${IMAGE_TAG} + ports: + - containerPort: 5432 + protocol: TCP + resources: + requests: + cpu: ${CPU_REQUEST} + memory: ${MEMORY_REQUEST} + limits: + cpu: ${CPU_LIMIT} + memory: ${MEMORY_LIMIT} + readinessProbe: + exec: + command: + - /usr/bin/env + - bash + - "-c" + - psql -q -U $POSTGRES_USER -d $POSTGRES_DB -c 'SELECT 1' + successThreshold: 1 + failureThreshold: 5 + initialDelaySeconds: 10 + periodSeconds: 15 + timeoutSeconds: 10 + livenessProbe: + exec: + command: + - /usr/bin/env + - bash + - "-c" + - psql -q $POSTGRES_USER -d $POSTGRES_DB -c 'SELECT 1' + successThreshold: 1 + failureThreshold: 5 + initialDelaySeconds: 10 + periodSeconds: 15 + timeoutSeconds: 10 + env: + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: ${NAME}-${ZONE}-${COMPONENT} + key: database-name + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: ${NAME}-${ZONE}-${COMPONENT} + key: database-password + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: ${NAME}-${ZONE}-${COMPONENT} + key: database-user + volumeMounts: + - name: ${NAME}-${ZONE}-${COMPONENT} + mountPath: ${PVC_MOUNT_PATH} + terminationMessagePath: "/dev/termination-log" + terminationMessagePolicy: File + imagePullPolicy: Always + restartPolicy: Always + terminationGracePeriodSeconds: 30 + dnsPolicy: ClusterFirst + schedulerName: default-scheduler + - apiVersion: v1 + kind: Service + metadata: + labels: + app: ${NAME}-${ZONE} + name: ${NAME}-${ZONE}-${COMPONENT} + spec: + ports: + - name: postgresql + nodePort: 0 + port: 5432 + protocol: TCP + targetPort: 5432 + selector: + deploymentconfig: ${NAME}-${ZONE}-${COMPONENT} + sessionAffinity: None + type: ClusterIP \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 416eecfa..084e5091 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,11 @@ x-backend: &backend retries: 5 restart: always +x-db-vars: &db-vars + POSTGRES_USER: postgres + POSTGRES_PASSWORD: default + POSTGRES_DB: postgres + services: frontend: container_name: frontend @@ -41,10 +46,16 @@ services: backend: container_name: backend + depends_on: + database: + condition: service_healthy entrypoint: mvn -ntp spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n" image: maven:3.9.4-amazoncorretto-21 volumes: ["./backend:/app"] working_dir: /app + environment: + POSTGRES_HOST: database + <<: *db-vars <<: *backend backend-native: @@ -52,3 +63,16 @@ services: profiles: ["native"] build: ./backend <<: *backend + + database: + container_name: database + environment: + <<: *db-vars + volumes: ["/pgdata"] + ports: ["5432:5432"] + healthcheck: + test: pg_isready -U postgres + interval: 5s + timeout: 5s + retries: 5 + image: postgis/postgis:13-master