diff --git a/.github/actions/build-flavor/action.yml b/.github/actions/build-flavor/action.yml index da2271a..4aacfd0 100644 --- a/.github/actions/build-flavor/action.yml +++ b/.github/actions/build-flavor/action.yml @@ -17,6 +17,12 @@ inputs: Token for the container registry. A composite action has no secrets context, so this has to be passed in by the calling workflow. required: true + repo_client_crt: + description: PEM contents of the client certificate for the gated pgEdge repositories. + required: true + repo_client_key: + description: PEM contents of the matching private key. + required: true dry_run: description: When true, resolve and print the build without pushing required: false @@ -61,6 +67,23 @@ runs: go-version: '1.25' cache-dependency-path: tests/go.sum + # Written to files because bake passes secrets by path. BuildKit mounts + # them into the RUNs that install packages and never writes them to a + # layer, so the published image carries no credentials. + - name: Write the repository client certificate + shell: bash + env: + REPO_CLIENT_CRT_PEM: ${{ inputs.repo_client_crt }} + REPO_CLIENT_KEY_PEM: ${{ inputs.repo_client_key }} + run: | + set -o errexit -o pipefail -o nounset + : "${REPO_CLIENT_CRT_PEM:?is empty or unset}" + : "${REPO_CLIENT_KEY_PEM:?is empty or unset}" + install -d -m 0700 "${RUNNER_TEMP}/pgedge-certs" + install -m 0600 /dev/null "${RUNNER_TEMP}/pgedge-certs/client.key" + printf '%s\n' "$REPO_CLIENT_CRT_PEM" > "${RUNNER_TEMP}/pgedge-certs/client.crt" + printf '%s\n' "$REPO_CLIENT_KEY_PEM" > "${RUNNER_TEMP}/pgedge-certs/client.key" + - name: Build and push by digest id: build shell: bash @@ -79,6 +102,10 @@ runs: args=( --file pgedge.docker-bake.hcl + # The certificate lives outside the bake file's directory, so bake + # needs the entitlement to read it. Scoped to the directory the step + # above creates rather than all of RUNNER_TEMP. + --allow "fs.read=${RUNNER_TEMP}/pgedge-certs" --set "default.platform=linux/${arch}" --set "default.tags=" --metadata-file metadata.json @@ -99,6 +126,8 @@ runs: "POSTGRES_MAJOR_VERSION=$(jq -r '.postgres_major' <<< "$CELL")" "TARGET=${target}" "TAG=${REPO}" + "REPO_CLIENT_CRT=${RUNNER_TEMP}/pgedge-certs/client.crt" + "REPO_CLIENT_KEY=${RUNNER_TEMP}/pgedge-certs/client.key" ) while IFS= read -r kv; do envs+=("$kv") diff --git a/.github/workflows/build_images.yaml b/.github/workflows/build_images.yaml index eb75275..ce7b9c3 100644 --- a/.github/workflows/build_images.yaml +++ b/.github/workflows/build_images.yaml @@ -132,6 +132,8 @@ jobs: cell: ${{ toJSON(matrix.cell) }} repo: ${{ inputs.pgedge_image_repo }} registry_token: ${{ secrets.GITHUB_TOKEN }} + repo_client_crt: ${{ secrets.PGEDGE_REPO_CLIENT_CRT }} + repo_client_key: ${{ secrets.PGEDGE_REPO_CLIENT_KEY }} dry_run: ${{ inputs.pgedge_image_dry_run }} no_cache: ${{ inputs.pgedge_image_no_cache }} @@ -190,6 +192,8 @@ jobs: cell: ${{ toJSON(matrix.cell) }} repo: ${{ inputs.pgedge_image_repo }} registry_token: ${{ secrets.GITHUB_TOKEN }} + repo_client_crt: ${{ secrets.PGEDGE_REPO_CLIENT_CRT }} + repo_client_key: ${{ secrets.PGEDGE_REPO_CLIENT_KEY }} dry_run: ${{ inputs.pgedge_image_dry_run }} no_cache: ${{ inputs.pgedge_image_no_cache }} @@ -246,6 +250,8 @@ jobs: cell: ${{ toJSON(matrix.cell) }} repo: ${{ inputs.pgedge_image_repo }} registry_token: ${{ secrets.GITHUB_TOKEN }} + repo_client_crt: ${{ secrets.PGEDGE_REPO_CLIENT_CRT }} + repo_client_key: ${{ secrets.PGEDGE_REPO_CLIENT_KEY }} dry_run: ${{ inputs.pgedge_image_dry_run }} no_cache: ${{ inputs.pgedge_image_no_cache }} @@ -307,6 +313,8 @@ jobs: cell: ${{ toJSON(matrix.cell) }} repo: ${{ inputs.pgedge_image_repo }} registry_token: ${{ secrets.GITHUB_TOKEN }} + repo_client_crt: ${{ secrets.PGEDGE_REPO_CLIENT_CRT }} + repo_client_key: ${{ secrets.PGEDGE_REPO_CLIENT_KEY }} dry_run: ${{ inputs.pgedge_image_dry_run }} no_cache: ${{ inputs.pgedge_image_no_cache }} diff --git a/.gitignore b/.gitignore index 05a3223..1840b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,8 @@ tests/tests # Scratch written by the image build (bake metadata, per-arch digests) digests/ metadata.json + +# Default paths for the gated-repository client certificate in +# pgedge.docker-bake.hcl. Never commit these. +client.crt +client.key diff --git a/Dockerfile b/Dockerfile index 056a15f..0882d2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,9 @@ useradd -u ${POSTGRES_USER_ID} -m postgres -s /bin/bash dnf install -y epel-release dnf dnf config-manager --set-enabled crb dnf update -y --allowerasing -dnf install -y https://dnf.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm +# downloads.pgedge.com, not dnf.pgedge.com: the gated repository cannot serve +# the package that configures access to it. +dnf install -y https://downloads.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm if [[ -n "${PACKAGE_RELEASE_CHANNEL}" ]]; then sed -i "s|release|${PACKAGE_RELEASE_CHANNEL}|g" /etc/yum.repos.d/pgedge.repo fi @@ -51,13 +53,35 @@ ARG POSTGRES_MAJOR_VERSION COPY packagelists/${TARGETARCH}/${POSTGRES_PACKAGE_LIST_FILE} /usr/share/pgedge/packages.txt -RUN </dev/null 2>&1 || true' EXIT + +# A chained stage starts FROM a published parent built before the repositories +# were gated: its pgedge-release predates pgedge-subscription-manager and its +# repository definition is enabled, pointing at a host it can no longer reach. +# Disable those definitions and replace the package first, so neither this +# install nor the channel rewrite below depends on when the parent was built. +dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true +command -v pgedge-subscription-manager >/dev/null 2>&1 \ + || dnf install -y --disablerepo="pgedge*" \ + https://downloads.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm + + +pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force + xargs dnf install -y < /usr/share/pgedge/packages.txt # Patch any OS packages (including transitive dependencies pulled in above) # to the latest available errata so the image ships with security fixes. @@ -119,13 +143,33 @@ USER root COPY packagelists/${TARGETARCH}/${PACKAGE_LIST_FILE} /usr/share/pgedge/packages.txt -RUN </dev/null 2>&1 || true' EXIT + +# A chained stage starts FROM a published parent built before the repositories +# were gated: its pgedge-release predates pgedge-subscription-manager and its +# repository definition is enabled, pointing at a host it can no longer reach. +# Disable those definitions and replace the package first, so neither this +# install nor the channel rewrite below depends on when the parent was built. +dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true +command -v pgedge-subscription-manager >/dev/null 2>&1 \ + || dnf install -y --disablerepo="pgedge*" \ + https://downloads.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm + + # The repo config is inherited from the parent image, which may have been built # from a different channel, so re-point it here rather than relying on the base # stage. Idempotent: once rewritten there is no "release" left to match. @@ -135,6 +179,8 @@ fi # A delta: re-pinning a package the parent's "dnf update -y" has moved past # would be a downgrade, which dnf refuses. +pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force + xargs dnf install -y < /usr/share/pgedge/packages.txt dnf update -y dnf clean all @@ -161,13 +207,33 @@ USER root COPY packagelists/${TARGETARCH}/${STANDARD_PACKAGE_LIST_FILE} /usr/share/pgedge/packages.txt -RUN </dev/null 2>&1 || true' EXIT + +# A chained stage starts FROM a published parent built before the repositories +# were gated: its pgedge-release predates pgedge-subscription-manager and its +# repository definition is enabled, pointing at a host it can no longer reach. +# Disable those definitions and replace the package first, so neither this +# install nor the channel rewrite below depends on when the parent was built. +dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true +command -v pgedge-subscription-manager >/dev/null 2>&1 \ + || dnf install -y --disablerepo="pgedge*" \ + https://downloads.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm + + # The repo config is inherited from the parent image, which may have been built # from a different channel, so re-point it here rather than relying on the base # stage. Idempotent: once rewritten there is no "release" left to match. @@ -177,6 +243,8 @@ fi # A delta: re-pinning a package the parent's "dnf update -y" has moved past # would be a downgrade, which dnf refuses. +pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force + xargs dnf install -y < /usr/share/pgedge/packages.txt # Patch any OS packages (including transitive dependencies pulled in above) # to the latest available errata so the image ships with security fixes. @@ -265,13 +333,33 @@ USER root COPY packagelists/${TARGETARCH}/${COLDFRONT_PACKAGE_LIST_FILE} /usr/share/pgedge/coldfront-packages.txt -RUN </dev/null 2>&1 || true' EXIT + +# A chained stage starts FROM a published parent built before the repositories +# were gated: its pgedge-release predates pgedge-subscription-manager and its +# repository definition is enabled, pointing at a host it can no longer reach. +# Disable those definitions and replace the package first, so neither this +# install nor the channel rewrite below depends on when the parent was built. +dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true +command -v pgedge-subscription-manager >/dev/null 2>&1 \ + || dnf install -y --disablerepo="pgedge*" \ + https://downloads.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm + + # The repo config is inherited from the parent image, which may have been built # from a different channel, so re-point it here rather than relying on the base # stage. Idempotent: once rewritten there is no "release" left to match. @@ -281,6 +369,8 @@ fi # Dependencies are dnf's to resolve. No second "dnf update -y": that would move # ColdFront past its pinned NVR. +pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force + xargs dnf install -y --setopt=install_weak_deps=False < /usr/share/pgedge/coldfront-packages.txt dnf clean all diff --git a/pgedge.docker-bake.hcl b/pgedge.docker-bake.hcl index 12c81a3..26dadd9 100644 --- a/pgedge.docker-bake.hcl +++ b/pgedge.docker-bake.hcl @@ -63,6 +63,16 @@ variable "TAG" { default = "pgedge" } +variable "REPO_CLIENT_CRT" { + type = string + default = "client.crt" +} + +variable "REPO_CLIENT_KEY" { + type = string + default = "client.key" +} + target "default" { pull = true target = TARGET @@ -78,6 +88,14 @@ target "default" { STANDARD_IMAGE = STANDARD_IMAGE POSTGRES_MAJOR_VERSION = POSTGRES_MAJOR_VERSION } + // The pgEdge repositories authenticate clients with mutual TLS. BuildKit + // mounts these for the RUNs that install packages and never writes them to + // a layer. Point them at your own files with REPO_CLIENT_CRT and + // REPO_CLIENT_KEY. + secret = [ + "id=repo_client_crt,src=${REPO_CLIENT_CRT}", + "id=repo_client_key,src=${REPO_CLIENT_KEY}", + ] platforms = [ "linux/amd64", "linux/arm64",