Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions .github/workflows/pr_test_latest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: PR Test Latest Images

# This workflow runs on pull requests and tests the latest images
# from the internal repository to ensure they still work with any changes.

on:
pull_request:
branches:
- main

permissions:
contents: read
packages: read

env:
IMAGE_REGISTRY: ghcr.io/pgedge
PACKAGE_REPOSITORY: pgedge-postgres-internal

jobs:
# Get latest tags and generate test matrix
setup:
# Skip for forked PRs since they won't have access to internal packages
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
tags: ${{ steps.get-tags.outputs.tags }}
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Get latest tags
id: get-tags
run: |
set -e
TAGS=$(make latest-tags)

# Validate that tags were retrieved successfully
if [ -z "$TAGS" ]; then
echo "Error: Failed to retrieve latest tags. make latest-tags returned empty output."
exit 1
fi

echo "tags=$TAGS" >> $GITHUB_OUTPUT
echo "Latest tags: $TAGS"

- name: Generate test matrix
id: generate
run: |
# Parse tags from make output
TAGS="${{ steps.get-tags.outputs.tags }}"
IFS=',' read -ra TAG_ARRAY <<< "$TAGS"

# Test on both architectures
ARCHS=("x86" "arm")

# Build matrix JSON
matrix_items=""
for tag in "${TAG_ARRAY[@]}"; do
tag=$(echo "$tag" | xargs) # trim whitespace

# Determine flavor from tag
if [[ "$tag" == *"-minimal"* ]]; then
flavor="minimal"
elif [[ "$tag" == *"-standard"* ]]; then
flavor="standard"
else
# Default to standard if not specified
flavor="standard"
fi

for arch in "${ARCHS[@]}"; do
arch=$(echo "$arch" | xargs) # trim whitespace

# Map user-friendly arch names to runner names
if [[ "$arch" == "arm" ]] || [[ "$arch" == "arm64" ]]; then
runner="ubuntu-24.04-arm"
arch_display="arm"
elif [[ "$arch" == "x86" ]] || [[ "$arch" == "amd64" ]]; then
runner="ubuntu-latest"
arch_display="x86"
else
echo "Error: Unknown architecture '$arch'. Use 'x86' or 'arm'"
exit 1
fi

if [[ -n "$matrix_items" ]]; then
matrix_items+=","
fi
matrix_items+="{\"tag\":\"$tag\",\"arch\":\"$arch_display\",\"flavor\":\"$flavor\",\"runner\":\"$runner\"}"
done
done

echo "matrix={\"include\":[$matrix_items]}" >> $GITHUB_OUTPUT
echo "Generated matrix: {\"include\":[$matrix_items]}"

test:
# Skip for forked PRs since they won't have access to internal packages
if: github.event.pull_request.head.repo.full_name == github.repository
needs: setup
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache-dependency-path: tests/go.sum

- name: Pull image
run: |
IMAGE="${{ env.IMAGE_REGISTRY }}/${{ env.PACKAGE_REPOSITORY }}:${{ matrix.tag }}"
echo "Pulling image: $IMAGE"
docker pull "$IMAGE"

- name: Run tests
run: |
IMAGE="${{ env.IMAGE_REGISTRY }}/${{ env.PACKAGE_REPOSITORY }}:${{ matrix.tag }}"
make test-image IMAGE="$IMAGE" FLAVOR="${{ matrix.flavor }}"

results:
# Skip for forked PRs since they won't have access to internal packages
# Also use always() to ensure results are shown even if test job fails
if: github.event.pull_request.head.repo.full_name == github.repository && always()
needs: [setup, test]
runs-on: ubuntu-latest
steps:
- name: Output
run: |
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Input | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Package Repository | ${{ env.PACKAGE_REPOSITORY }} |" >> $GITHUB_STEP_SUMMARY
echo "| Tags | ${{ needs.setup.outputs.tags }} |" >> $GITHUB_STEP_SUMMARY
echo "| Architectures | x86,arm |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

if [[ "${{ needs.test.result }}" == "success" ]]; then
echo "✅ **All tests passed!**" >> $GITHUB_STEP_SUMMARY
else
echo "❌ **Some tests failed.** Check the job logs for details." >> $GITHUB_STEP_SUMMARY
fi
36 changes: 17 additions & 19 deletions .github/workflows/test_images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ on:
type: string
required: true
architectures:
description: "Comma-separated list of architectures to test (amd64,arm64)"
description: "Comma-separated list of architectures to test (x86,arm)"
type: string
default: "amd64,arm64"
default: "x86,arm"
required: false

permissions:
Expand Down Expand Up @@ -62,17 +62,24 @@ jobs:
for arch in "${ARCHS[@]}"; do
arch=$(echo "$arch" | xargs) # trim whitespace

# Determine runner based on architecture
if [[ "$arch" == "arm64" ]]; then
runner="ubuntu-24.04-arm64"
else
# Map user-friendly arch names to runner names
# Accept "arm" or "arm64" -> use ubuntu-24.04-arm
# Accept "x86" or "amd64" -> use ubuntu-latest
if [[ "$arch" == "arm" ]] || [[ "$arch" == "arm64" ]]; then
runner="ubuntu-24.04-arm"
arch_display="arm"
elif [[ "$arch" == "x86" ]] || [[ "$arch" == "amd64" ]]; then
runner="ubuntu-latest"
arch_display="x86"
else
echo "Error: Unknown architecture '$arch'. Use 'x86' or 'arm'"
exit 1
fi

if [[ -n "$matrix_items" ]]; then
matrix_items+=","
fi
matrix_items+="{\"tag\":\"$tag\",\"arch\":\"$arch\",\"flavor\":\"$flavor\",\"runner\":\"$runner\"}"
matrix_items+="{\"tag\":\"$tag\",\"arch\":\"$arch_display\",\"flavor\":\"$flavor\",\"runner\":\"$runner\"}"
done
done

Expand All @@ -92,16 +99,9 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24.11'
go-version: '1.23'
cache-dependency-path: tests/go.sum

- name: Login to GitHub Container Registry
env:
GH_USER: ${{ github.actor }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "${GH_TOKEN}" | docker login ghcr.io -u "${GH_USER}" --password-stdin

- name: Pull image
run: |
IMAGE="${{ env.IMAGE_REGISTRY }}/${{ inputs.package_repository }}:${{ matrix.tag }}"
Expand All @@ -113,15 +113,13 @@ jobs:
IMAGE="${{ env.IMAGE_REGISTRY }}/${{ inputs.package_repository }}:${{ matrix.tag }}"
make test-image IMAGE="$IMAGE" FLAVOR="${{ matrix.flavor }}"

summary:
results:
needs: [setup, test]
runs-on: ubuntu-latest
if: always()
steps:
- name: Test Summary
- name: Output
run: |
echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Input | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Package Repository | ${{ inputs.package_repository }} |" >> $GITHUB_STEP_SUMMARY
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,6 @@ ifndef FLAVOR
endif
cd tests && go run main.go -image $(IMAGE) -flavor $(FLAVOR)

.PHONY: latest-tags
latest-tags:
@PGEDGE_LIST_LATEST_TAGS=1 ./scripts/build_pgedge_images.py
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,60 @@ volumes:
- Mutable tags also exist for:
- The latest image for a given Postgres major.minor + spock major version, `pg<postgres major.minor>-spock<major>-<flavor>` , e.g. `17.6-spock5-standard`
- The latest image for a given Postgres major + spock major version, `pg<postgres major>-spock<major>-<flavor>`, e.g. `17-spock5-standard`

## Testing

This repository includes a comprehensive test suite to validate Postgres images. The tests verify:
- Default entrypoint functionality
- Patroni entrypoint (standard images only)
- PostgreSQL connectivity and version checks
- Extension availability and functionality (Spock, LOLOR, Snowflake, pgvector, PostGIS, pgaudit)
- pgBackRest installation (standard images only)

### Running Tests Locally

To run tests locally, you'll need:
- Go 1.24.11 or later
- Docker installed and running
- Access to pull the image you want to test

Run the test suite using the Makefile:

```bash
make test-image IMAGE=<image> FLAVOR=<minimal|standard>
```

Example:

```bash
make test-image IMAGE=ghcr.io/pgedge/pgedge-postgres:17-spock5-standard FLAVOR=standard
```

Or run directly with Go:

```bash
cd tests && go run main.go -image <image> -flavor <minimal|standard>
```

### Local Testing Limitations

**Architecture Limitations:** When running tests locally, you can only test images that match your local machine's architecture. For example:
- On an x86_64/amd64 machine, you can only test amd64 images
- On an ARM64 machine, you can only test arm64 images

To test images for multiple architectures, use the GitHub Actions workflow which runs tests on architecture-specific runners:
- `ubuntu-latest` runner (amd64/x86_64 architecture)
- `ubuntu-24.04-arm` runner (arm64 architecture)

### CI/CD Testing

The GitHub Actions workflow (`.github/workflows/test_images.yaml`) can be triggered manually to test images across multiple architectures. The workflow uses specific runner labels to target CPU architectures:
- **amd64/x86_64**: Uses `ubuntu-latest` runner
- **arm64**: Uses `ubuntu-24.04-arm` runner

The workflow accepts:
- **Package Repository**: The container registry repository name
- **Tags**: Comma-separated list of image tags to test
- **Architectures**: Comma-separated list of architectures (`x86,arm` or `amd64,arm64`)

The workflow will automatically test each tag on each specified architecture by mapping the architecture names to the appropriate runner labels.
36 changes: 35 additions & 1 deletion docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,41 @@ docker_init_database_dir() {
fi

# --pwfile refuses to handle a properly-empty file (hence the "\n"): https://github.com/docker-library/postgres/issues/1025
eval 'initdb --username="$POSTGRES_USER" --pwfile=<(printf "%s\n" "$POSTGRES_PASSWORD") '"$POSTGRES_INITDB_ARGS"' "$@"'
# Create a temporary password file to avoid command injection via POSTGRES_INITDB_ARGS
local pwfile
pwfile="$(mktemp)"
# Ensure cleanup on exit (including errors) to prevent password file from being left on disk
trap 'rm -f "$pwfile"' EXIT
printf '%s\n' "$POSTGRES_PASSWORD" > "$pwfile"

# Build initdb command arguments safely using an array
local initdb_args=(
--username="$POSTGRES_USER"
--pwfile="$pwfile"
)

# Safely parse POSTGRES_INITDB_ARGS if it exists
# Use read -a to split the string into an array without shell interpretation
# This prevents command injection while preserving argument structure
if [ -n "${POSTGRES_INITDB_ARGS:-}" ]; then
# Read the arguments into an array, splitting on whitespace
# Note: This does not handle quoted arguments with spaces.
# For complex cases, pass arguments as function parameters instead.
local args_array
IFS=' ' read -r -a args_array <<< "$POSTGRES_INITDB_ARGS"
initdb_args+=("${args_array[@]}")
fi

# Add any function arguments (these are already safely parsed by the shell)
initdb_args+=("$@")

# Execute initdb with the safely constructed arguments
initdb "${initdb_args[@]}"

# Clean up temporary password file (trap will also handle this on exit, but explicit cleanup is good)
rm -f "$pwfile"
# Remove the trap since we've cleaned up manually
trap - EXIT

# unset/cleanup "nss_wrapper" bits
if [[ "${LD_PRELOAD:-}" == */libnss_wrapper.so ]]; then
Expand Down
23 changes: 23 additions & 0 deletions scripts/build_pgedge_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Config:
only_postgres_version: str
only_spock_version: str
only_arch: str
list_latest_tags: bool

@staticmethod
def from_env() -> "Config":
Expand All @@ -29,6 +30,7 @@ def from_env() -> "Config":
only_postgres_version=os.getenv("PGEDGE_IMAGE_ONLY_POSTGRES_VERSION", ""),
only_spock_version=os.getenv("PGEDGE_IMAGE_ONLY_SPOCK_VERSION", ""),
only_arch=os.getenv("PGEDGE_IMAGE_ONLY_ARCH", ""),
list_latest_tags=(os.getenv("PGEDGE_LIST_LATEST_TAGS", "0") == "1"),
)


Expand Down Expand Up @@ -324,6 +326,13 @@ def main():

config = Config.from_env()

# If list_latest_tags is enabled, output tags and exit
if config.list_latest_tags:
tags = get_latest_tags()
# Output as comma-separated list
print(",".join(tags))
return

if config.dry_run:
logging.info("dry run enabled. build and publish actions will be skipped.")

Expand Down Expand Up @@ -397,5 +406,19 @@ def main():
logging.info(f"{tag} is already up-to-date")


def get_latest_tags() -> list[str]:
"""
Returns a list of the latest immutable tags (with epoch and pg minor version)
for all images that are marked as latest for their Postgres major version.
Returns tags in the format: {postgres_version}-spock{spock_version}-{flavor}-{epoch}
"""
latest_tags = []
for image in all_images:
if image.is_latest_for_pg_major:
# Get the immutable build tag with epoch and full postgres version
latest_tags.append(str(image.build_tag))
return latest_tags


if __name__ == "__main__":
main()
Loading