diff --git a/.github/TEMPLATES/secret-mapping-opencrvs-deps.yml b/.github/TEMPLATES/secret-mapping-opencrvs-deps.yml index ae4343cb..dbcf73af 100644 --- a/.github/TEMPLATES/secret-mapping-opencrvs-deps.yml +++ b/.github/TEMPLATES/secret-mapping-opencrvs-deps.yml @@ -40,8 +40,8 @@ traefik-cert: type: tls namespace: traefik data: - - SSL_CRT: cert - - SSL_KEY: key + - SSL_CRT: tls.crt + - SSL_KEY: tls.key # If backup is configured then workflow will use GitHub secrets for current environment # If restore is configured then workflow will fetch secrets from source environment (usually production) diff --git a/.github/workflows/deploy-dependencies.yml b/.github/workflows/deploy-dependencies.yml index 9a927ab6..c95ffa02 100644 --- a/.github/workflows/deploy-dependencies.yml +++ b/.github/workflows/deploy-dependencies.yml @@ -11,7 +11,7 @@ on: options: - "" env: - DEPENDENCIES_CHART_VERSION: "v1.9.12" + DEPENDENCIES_CHART_VERSION: "1.9.15" TRAEFIK_CHART_VERSION: "39.0.0" jobs: approve: @@ -64,7 +64,7 @@ jobs: --create-namespace \ -f environments/${ENV}/traefik/values.yaml \ -f environments/${ENV}/traefik/values.override.yaml - kubectl scale deployment traefik --replicas=1 --namespace traefik + kubectl scale deployment traefik --replicas=1 --namespace traefik || true - name: Install OpenCRVS dependencies id: deploy diff --git a/.github/workflows/deploy-opencrvs.yml b/.github/workflows/deploy-opencrvs.yml index a00daca1..a185754c 100644 --- a/.github/workflows/deploy-opencrvs.yml +++ b/.github/workflows/deploy-opencrvs.yml @@ -16,11 +16,11 @@ on: core-image-tag: description: "Tag of the core image" required: true - default: "v1.9.12" + default: "1.9.15" countryconfig-image-tag: description: "Tag of the countryconfig image" required: true - default: "v1.9.12" + default: "1.9.15" data-seed-enabled: description: "Data seeding during deployment" required: false @@ -36,7 +36,7 @@ on: env: # Assuming chart version matches core image tag - OPENCRVS_CHART_VERSION: "v1.9.12" + OPENCRVS_CHART_VERSION: "1.9.15" jobs: approve: environment: ${{ inputs.environment }} @@ -145,8 +145,8 @@ jobs: --atomic \ --wait \ --wait-for-jobs \ - --set image.tag="$CORE_IMAGE_TAG" \ - --set countryconfig.image.tag="$COUNTRYCONFIG_IMAGE_TAG" \ + --set-string platform.tag="$CORE_IMAGE_TAG" \ + --set-string countryconfig.image.tag="$COUNTRYCONFIG_IMAGE_TAG" \ --set countryconfig.image.name="$COUNTRYCONFIG_IMAGE_NAME" \ --set data_seed.env.ACTIVATE_USERS="${{ vars.ACTIVATE_USERS || 'false' }}" \ --set data_seed.enabled="${{ inputs.data-seed-enabled }}" \ diff --git a/.github/workflows/github-to-k8s-sync-env.yml b/.github/workflows/github-to-k8s-sync-env.yml index cf7c286a..19f49398 100644 --- a/.github/workflows/github-to-k8s-sync-env.yml +++ b/.github/workflows/github-to-k8s-sync-env.yml @@ -199,8 +199,6 @@ jobs: echo BACKUP_HOST=$BACKUP_HOST >> $env_file echo BACKUP_HOST_PRIVATE_KEY=$BACKUP_HOST_PRIVATE_KEY >> $env_file echo BACKUP_SERVER_USER=$BACKUP_SERVER_USER >> $env_file - echo POSTGRES_USER=$POSTGRES_USER >> $env_file - echo POSTGRES_PASSWORD=$POSTGRES_PASSWORD >> $env_file grep BACKUP_HOST_PRIVATE_KEY $env_file || echo "No restore encryption passphrase to add" - name: Preprocess mapping into Secret YAMLs @@ -293,6 +291,7 @@ jobs: echo "🚀 Applying $f within namespace" kubectl get namespace $namespace >/dev/null 2>&1 || kubectl create namespace $namespace yq eval . "$f" > /dev/null && echo "✅ $f is valid YAML" || echo "❌ $f has YAML errors" && cat "$f" + kubectl delete --ignore-not-found -n $namespace -f "$f" kubectl apply -n $namespace -f "$f" echo "Dropping last-applied-configuration annotation for secret $(basename ${f/.yaml/})" kubectl annotate secret -n $namespace "$(basename ${f/.yaml/})" kubectl.kubernetes.io/last-applied-configuration- diff --git a/.github/workflows/init-release.yml b/.github/workflows/init-release.yml new file mode 100644 index 00000000..a8a175bb --- /dev/null +++ b/.github/workflows/init-release.yml @@ -0,0 +1,216 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# +# OpenCRVS is also distributed under the terms of the Civil Registration +# & Healthcare Disclaimer located at http://opencrvs.org/license. +# +# Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + +# Gitflow release initialisation workflow. +# +# Triggered by opencrvs-core's init-release workflow (or manually) with a +# semver version string (e.g. 1.7.2). +# +# What this workflow does, in order: +# 1. Derives the base branch: +# - patch release (z > 0) → release/.. +# - minor/major (z = 0) → develop +# 2. Creates a release branch (release/) from the base branch. +# 3. Bumps Chart.yaml and prepends a CHANGELOG section, then pushes +# both commits to the release branch. +# 4. Opens two PRs — both PRs already contain the version-bump commits: +# a. release/ → master (the release PR) +# b. release/ → develop (merge-back PR, keeps develop in sync) +# 5. Creates a draft GitHub release and immediately deletes the backing tag +# (the tag will be re-created by the publish workflow after the PR merges). +name: 11. Release - Start a new release +on: + workflow_dispatch: + inputs: + version: + type: string + required: true + description: 'Version to release' + +jobs: + release_workflow: + runs-on: ubuntu-latest + steps: + # Derive base_branch and target_branch from the version number. + # - patch (z > 0): base = release/.., target = master + # - minor/major (z = 0): base = develop, target = master + - name: Determine the Base Branch + id: get_base_branch + run: | + version="${{ inputs.version }}" + + if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid version was provided as input: $version" >&2 + echo "Please follow semver format for versioning, e/g 1.7.2" + exit 1 + fi + + IFS='.' read -r x y z <<< "$version" + if [ "$z" = "0" ]; then + base_branch="develop" + else + previous_version="$((x)).$((y)).$((z-1))" + base_branch="release/$previous_version" + fi + echo "target_branch=master" >> $GITHUB_OUTPUT + echo "base_branch=$base_branch" >> $GITHUB_OUTPUT + + # Full history is required so yarn can traverse the commit graph. + - name: Checkout ${{ steps.get_base_branch.outputs.base_branch }} + uses: actions/checkout@v4 + with: + ref: ${{ steps.get_base_branch.outputs.base_branch }} + fetch-depth: 0 + token: ${{ secrets.INFRASTRUCTURE_WORKFLOW_TOKEN }} + + - name: Check if release branch already exists + id: check_branch + run: | + if git ls-remote --exit-code --heads origin "release/${{ inputs.version }}" > /dev/null 2>&1; then + echo "Branch release/${{ inputs.version }} already exists, skipping creation." + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + # Create the release branch, bump helm chart version, and update the + # CHANGELOG — then push. The PRs are opened only after these commits + # exist on the remote, so both PRs immediately include the version-bump + # commits. + # Skipped if the release branch was already created by a previous run. + - name: Create release branch and update version numbers and CHANGELOG + if: steps.check_branch.outputs.exists == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git checkout -b "release/${{ inputs.version }}" + export VERSION="${{ inputs.version }}" + # Bump helm chart version: sed instead of yq to preserve formatting and comments. + sed -i \ + -e "s/^\(\s*OPENCRVS_CHART_VERSION:\s*\).*/\1\"$VERSION\"/" \ + -e '/core-image-tag:/,/default:/ s/default: ".*"/default: "'"$VERSION"'"/' \ + -e '/countryconfig-image-tag:/,/default:/ s/default: ".*"/default: "'"$VERSION"'"/' \ + .github/workflows/deploy-opencrvs.yml + sed -i \ + -e "s/^\(\s*DEPENDENCIES_CHART_VERSION:\s*\).*/\1\"$VERSION\"/" \ + .github/workflows/deploy-dependencies.yml + git add . + git commit -m "chore: update version to ${{ inputs.version }}" + + # Prepend a new "Release Candidate" section to the top of CHANGELOG.md. + # sed removes the existing "# Changelog" header (lines 1-2) so we can + # rewrite it with the new section underneath. + sed -i '1,2d' CHANGELOG.md + cp CHANGELOG.md COPY_CHANGELOG.md + { + echo "# Changelog" + echo "" + echo "## ${{ inputs.version }} Release Candidate" + echo "" + cat COPY_CHANGELOG.md + } > CHANGELOG.md + rm COPY_CHANGELOG.md + git add CHANGELOG.md + git commit -m "docs: update changelog for ${{ inputs.version }} release candidate" + + git push origin "release/${{ inputs.version }}" + + # A dedicated merge-back branch is used for the develop PR so that any + # conflict-resolution commits do not land on the release branch itself. + - name: Create merge-back branch for develop PR + run: | + MERGEBACK_BRANCH="merge-back/release-${{ inputs.version }}-to-develop" + if git ls-remote --exit-code --heads origin "$MERGEBACK_BRANCH" > /dev/null 2>&1; then + echo "Branch $MERGEBACK_BRANCH already exists, skipping." + else + git fetch origin "release/${{ inputs.version }}" + git checkout -b "$MERGEBACK_BRANCH" "origin/release/${{ inputs.version }}" + git push origin "$MERGEBACK_BRANCH" + fi + + # Two PRs are always created: + # + # PR 1 — release/ → master + # The release PR. Merging this ships the version to production. + # + # PR 2 — merge-back/release--to-develop → develop + # Uses a dedicated branch so conflict-resolution commits stay off + # the release branch. + # - z=0 (base is develop): carries only the two bump commits, clean diff. + # - z>0 (base is a prior release branch): also backports any patch fixes. + # Each PR is checked individually — one may already exist while the other + # does not, so both are always evaluated. + - name: Create Pull Requests + run: | + PR_BRANCH="release/${{ inputs.version }}" + MERGEBACK_BRANCH="merge-back/release-${{ inputs.version }}-to-develop" + TARGET_BRANCH="${{ steps.get_base_branch.outputs.target_branch }}" + + if [ -f .github/TEMPLATES/RELEASE_PR_TEMPLATE.md ]; then + BODY=$(cat .github/TEMPLATES/RELEASE_PR_TEMPLATE.md) + else + BODY="" + fi + + EXISTING_MASTER_PR=$(gh pr list --head "$PR_BRANCH" --base "$TARGET_BRANCH" --state open --json number --jq '.[0].number // ""') + if [ -z "$EXISTING_MASTER_PR" ]; then + gh pr create \ + --base "$TARGET_BRANCH" \ + --head "$PR_BRANCH" \ + --title "Release v${{ inputs.version }}" \ + --body "$BODY" + else + echo "PR $PR_BRANCH → $TARGET_BRANCH already exists (#$EXISTING_MASTER_PR), skipping." + fi + + EXISTING_DEVELOP_PR=$(gh pr list --head "$MERGEBACK_BRANCH" --base "develop" --state open --json number --jq '.[0].number // ""') + if [ -z "$EXISTING_DEVELOP_PR" ]; then + gh pr create \ + --base "develop" \ + --head "$MERGEBACK_BRANCH" \ + --title "chore: merge release v${{ inputs.version }} back to develop" \ + --body "Merge release branch back to develop to include version bump and changelog updates." + else + echo "PR $MERGEBACK_BRANCH → develop already exists (#$EXISTING_DEVELOP_PR), skipping." + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Create a draft release so the release notes page exists immediately. + # The tag is deleted right after: the publish workflow re-creates it when + # the release PR is merged into master, which is the canonical tag point. + # Skipped if a release for this version already exists from a previous run. + - name: Create a draft release + run: | + TAG_NAME="v${{ inputs.version }}" + if gh release view "$TAG_NAME" > /dev/null 2>&1; then + echo "Release $TAG_NAME already exists, skipping." + else + TITLE="OpenCRVS - $TAG_NAME" + NOTES="Release notes for version ${{ inputs.version }}" + git tag "$TAG_NAME" + git push origin "$TAG_NAME" + gh release create "$TAG_NAME" \ + --title "$TITLE" \ + --notes "$NOTES" \ + --draft + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Delete the tag + run: | + TAG_NAME="v${{ inputs.version }}" + if git ls-remote --exit-code --tags origin "$TAG_NAME" > /dev/null 2>&1; then + git tag -d "$TAG_NAME" 2>/dev/null || true + git push origin ":refs/tags/$TAG_NAME" + else + echo "Tag $TAG_NAME does not exist on remote, skipping." + fi \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..8427d5a6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## 1.9.15 Release Candidate + +## 1.9.14 Release Candidate + +### Fixes + +- Improved internet connectivity checks by replacing ICMP ping with HTTPS endpoint validation and detailed diagnostics for restricted environments. \ No newline at end of file diff --git a/infrastructure/environments/templates/charts-values/opencrvs-services/values.yaml b/infrastructure/environments/templates/charts-values/opencrvs-services/values.yaml index def5a9f7..5356582a 100644 --- a/infrastructure/environments/templates/charts-values/opencrvs-services/values.yaml +++ b/infrastructure/environments/templates/charts-values/opencrvs-services/values.yaml @@ -49,10 +49,10 @@ redis: postgres: auth_mode: auto host: postgres-0.postgres.opencrvs-deps-{{env}}.svc.cluster.local - -imagePullSecrets: - # Default value for credentials created while yarn environment:init - - name: dockerhub-credentials +platform: + imagePullSecrets: + # Default value for credentials created while yarn environment:init + - name: dockerhub-credentials countryconfig: secrets: diff --git a/infrastructure/server-setup/tasks/all/tools.yml b/infrastructure/server-setup/tasks/all/tools.yml index 4662f39c..18e225d7 100644 --- a/infrastructure/server-setup/tasks/all/tools.yml +++ b/infrastructure/server-setup/tasks/all/tools.yml @@ -14,6 +14,7 @@ - zip - python3-pip - python3-kubernetes + - pigz - name: 'Ensure python-3pexpect is absent' apt: diff --git a/infrastructure/server-setup/tasks/k8s/ufw.yml b/infrastructure/server-setup/tasks/k8s/ufw.yml index 1d98043a..334d1673 100644 --- a/infrastructure/server-setup/tasks/k8s/ufw.yml +++ b/infrastructure/server-setup/tasks/k8s/ufw.yml @@ -36,6 +36,8 @@ - { port: 7946, proto: "udp" } # Used by some CNIs (e.g., Flannel, Weave) - { port: 8472, proto: "udp" } # VXLAN (Flannel/Calico; verify if needed) - { port: 4789, proto: "udp" } # VXLAN (Calico; verify if needed) + - { port: 179, proto: "tcp" } # BGP (Calico) + - { port: 179, proto: "udp" } # BGP (Calico) # Expose traefik on node port # Rules are required for internet facing load balancer (if exists) - { port: 30080, proto: "tcp" } # NodePort HTTP diff --git a/scripts/bootstrap/node-runner.sh b/scripts/bootstrap/node-runner.sh index a32184a4..1ee863f8 100644 --- a/scripts/bootstrap/node-runner.sh +++ b/scripts/bootstrap/node-runner.sh @@ -102,7 +102,7 @@ if [[ ! -f "runner.tar.gz" ]]; then fi echo "[+] Download URL: $RUNNER_LATEST_URL into folder $(pwd)" - if ! curl -fL "$RUNNER_LATEST_URL" -o runner.tar.gz; then + if ! sudo -u $RUNAS_USER curl -fL "$RUNNER_LATEST_URL" -o runner.tar.gz; then echo "❌ Failed to download runner archive." exit 1 fi @@ -111,9 +111,9 @@ else fi echo "[+] Extracting runner..." -tar xzf runner.tar.gz +sudo -u $RUNAS_USER tar xzf runner.tar.gz echo "[+] Setting permissions... `pwd`" -chown -R $RUNAS_USER:$RUNAS_GROUP . +sudo chown -R $RUNAS_USER:$RUNAS_GROUP . # --- GET REGISTRATION TOKEN --- echo "[+] Requesting registration token..." REG_TOKEN=$(curl -s -X POST \ @@ -133,7 +133,7 @@ sudo -u $RUNAS_USER ./config.sh \ # --- SETUP SYSTEMD SERVICE --- echo "[+] Installing systemd service..." -sudo ./svc.sh install +sudo ./svc.sh install provision # Fix service to run as specific user/group SERVICE_FILE_PATH=$(ls /etc/systemd/system/actions.runner.*.service 2>/dev/null | head -n1) diff --git a/scripts/bootstrap/opencrvs-bootstrap.sh b/scripts/bootstrap/opencrvs-bootstrap.sh index f066df7a..049687eb 100644 --- a/scripts/bootstrap/opencrvs-bootstrap.sh +++ b/scripts/bootstrap/opencrvs-bootstrap.sh @@ -69,12 +69,75 @@ check_ubuntu_version() { echo "Ubuntu version OK." } +curl_check_url() { + local url="$1" + local http_code + + http_code="$(curl \ + --silent \ + --location \ + --head \ + --retry 3 \ + --retry-delay 2 \ + --retry-all-errors \ + --max-time 10 \ + --output /dev/null \ + --write-out "%{http_code}" \ + "$url" || true)" + + # 000 means curl could not connect / DNS failed / TLS failed / timed out. + if [ "$http_code" = "000" ]; then + return 1 + fi + + return 0 +} check_internet() { - echo "Testing internet connectivity (ping google.com)..." - if ! ping -c 2 google.com >/dev/null 2>&1; then - abort "Internet connectivity failed (cannot reach google.com)" + local urls=( + "https://raw.githubusercontent.com/" + "https://get.helm.sh" + "https://pkgs.k8s.io" + "https://archive.ubuntu.com" + "https://changelogs.ubuntu.com" + "https://hub.docker.com" + "https://auth.docker.io" + "https://registry-1.docker.io" + "https://download.docker.com" + "https://sentry.io" + "https://fonts.gstatic.com" + "https://storage.googleapis.com" + "https://fonts.googleapis.com" + "https://github.com" + "https://acme-v02.api.letsencrypt.org" + "https://registry.npmjs.org" + "https://registry.yarnpkg.com" + "https://eu.ui-avatars.com" + ) + + local failed=0 + + echo "Testing outbound HTTPS connectivity..." + echo + + printf "%-40s %-10s\n" "URL" "STATUS" + printf "%-40s %-10s\n" "----------------------------------------" "----------" + + for url in "${urls[@]}"; do + if curl_check_url "$url"; then + printf "%-45s %-10s\n" "$url" "OK" + else + printf "%-45s %-10s\n" "$url" "FAILED" + failed=1 + fi + done + + echo + + if [ "$failed" -ne 0 ]; then + abort "Internet connectivity check failed. Some required endpoints are unreachable." fi + echo "Internet connectivity OK." } diff --git a/scripts/tools/update-traefik-cert.sh b/scripts/tools/update-traefik-cert.sh new file mode 100644 index 00000000..d0a0f5bb --- /dev/null +++ b/scripts/tools/update-traefik-cert.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# +# OpenCRVS is also distributed under the terms of the Civil Registration +# & Healthcare Disclaimer located at http://opencrvs.org/license. +# +# Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS + +set -e + +CERT="" +KEY="" +NAMESPACE="traefik" +SECRET_NAME="traefik-cert" +BACKUP_SECRET=$(mktemp) + +NEW_HASH=$(sha256sum "$CERT" | cut -d' ' -f1) +OLD_HASH=$(kubectl get secret "$SECRET_NAME" --ignore-not-found -n "$NAMESPACE" -o jsonpath='{.data.tls\.crt}' | base64 -d | sha256sum | cut -d' ' -f1) +if [ "$NEW_HASH" == "$OLD_HASH" ]; then + logger -t traefik-cert "No changes..." + exit 0 +fi + +logger -t traefik-cert "Starting TLS secret update on secret $SECRET_NAME in namespace ${NAMESPACE}" + +kubectl get secret "$SECRET_NAME" -n "$NAMESPACE" -o yaml \ + | grep -vE 'resourceVersion|uid|creationTimestamp' > "$BACKUP_SECRET" && \ +logger -t traefik-cert "Backed up existing secret to $BACKUP_SECRET" && \ +kubectl delete secret -n $NAMESPACE $SECRET_NAME --ignore-not-found && \ +logger -t traefik-cert "Deleted existing secret" || \ +logger -p user.warn -t traefik-cert "Failed to backup and delete existing secret. Trying to proceed with update..." + +if kubectl create secret tls $SECRET_NAME \ + --cert=$CERT \ + --key=$KEY \ + -n $NAMESPACE \ + --dry-run=client -o yaml | kubectl apply -f -; then + logger -p user.info -t traefik-cert "TLS secret updated successfully" +else + logger -p user.err -t traefik-cert "TLS secret update FAILED" + kubectl apply -f $BACKUP_SECRET + logger -p user.warn -t traefik-cert "Restored previous secret version" +fi