Skip to content

Commit 98dd332

Browse files
authored
Plat-362:Add comprehensive test suite and PR workflow for postgres images
2 parents 0a75566 + 7fc74ea commit 98dd332

File tree

8 files changed

+588
-195
lines changed

8 files changed

+588
-195
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
name: PR Test Latest Images
2+
3+
# This workflow runs on pull requests and tests the latest images
4+
# from the internal repository to ensure they still work with any changes.
5+
6+
on:
7+
pull_request:
8+
branches:
9+
- main
10+
11+
permissions:
12+
contents: read
13+
packages: read
14+
15+
env:
16+
IMAGE_REGISTRY: ghcr.io/pgedge
17+
PACKAGE_REPOSITORY: pgedge-postgres-internal
18+
19+
jobs:
20+
# Get latest tags and generate test matrix
21+
setup:
22+
# Skip for forked PRs since they won't have access to internal packages
23+
if: github.event.pull_request.head.repo.full_name == github.repository
24+
runs-on: ubuntu-latest
25+
outputs:
26+
matrix: ${{ steps.generate.outputs.matrix }}
27+
tags: ${{ steps.get-tags.outputs.tags }}
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@v4
31+
32+
- name: Set up Python
33+
uses: actions/setup-python@v5
34+
with:
35+
python-version: '3.x'
36+
37+
- name: Get latest tags
38+
id: get-tags
39+
run: |
40+
set -e
41+
TAGS=$(make latest-tags)
42+
43+
# Validate that tags were retrieved successfully
44+
if [ -z "$TAGS" ]; then
45+
echo "Error: Failed to retrieve latest tags. make latest-tags returned empty output."
46+
exit 1
47+
fi
48+
49+
echo "tags=$TAGS" >> $GITHUB_OUTPUT
50+
echo "Latest tags: $TAGS"
51+
52+
- name: Generate test matrix
53+
id: generate
54+
run: |
55+
# Parse tags from make output
56+
TAGS="${{ steps.get-tags.outputs.tags }}"
57+
IFS=',' read -ra TAG_ARRAY <<< "$TAGS"
58+
59+
# Test on both architectures
60+
ARCHS=("x86" "arm")
61+
62+
# Build matrix JSON
63+
matrix_items=""
64+
for tag in "${TAG_ARRAY[@]}"; do
65+
tag=$(echo "$tag" | xargs) # trim whitespace
66+
67+
# Determine flavor from tag
68+
if [[ "$tag" == *"-minimal"* ]]; then
69+
flavor="minimal"
70+
elif [[ "$tag" == *"-standard"* ]]; then
71+
flavor="standard"
72+
else
73+
# Default to standard if not specified
74+
flavor="standard"
75+
fi
76+
77+
for arch in "${ARCHS[@]}"; do
78+
arch=$(echo "$arch" | xargs) # trim whitespace
79+
80+
# Map user-friendly arch names to runner names
81+
if [[ "$arch" == "arm" ]] || [[ "$arch" == "arm64" ]]; then
82+
runner="ubuntu-24.04-arm"
83+
arch_display="arm"
84+
elif [[ "$arch" == "x86" ]] || [[ "$arch" == "amd64" ]]; then
85+
runner="ubuntu-latest"
86+
arch_display="x86"
87+
else
88+
echo "Error: Unknown architecture '$arch'. Use 'x86' or 'arm'"
89+
exit 1
90+
fi
91+
92+
if [[ -n "$matrix_items" ]]; then
93+
matrix_items+=","
94+
fi
95+
matrix_items+="{\"tag\":\"$tag\",\"arch\":\"$arch_display\",\"flavor\":\"$flavor\",\"runner\":\"$runner\"}"
96+
done
97+
done
98+
99+
echo "matrix={\"include\":[$matrix_items]}" >> $GITHUB_OUTPUT
100+
echo "Generated matrix: {\"include\":[$matrix_items]}"
101+
102+
test:
103+
# Skip for forked PRs since they won't have access to internal packages
104+
if: github.event.pull_request.head.repo.full_name == github.repository
105+
needs: setup
106+
runs-on: ${{ matrix.runner }}
107+
strategy:
108+
fail-fast: false
109+
matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
110+
steps:
111+
- name: Checkout repository
112+
uses: actions/checkout@v4
113+
114+
- name: Set up Go
115+
uses: actions/setup-go@v5
116+
with:
117+
go-version: '1.23'
118+
cache-dependency-path: tests/go.sum
119+
120+
- name: Pull image
121+
run: |
122+
IMAGE="${{ env.IMAGE_REGISTRY }}/${{ env.PACKAGE_REPOSITORY }}:${{ matrix.tag }}"
123+
echo "Pulling image: $IMAGE"
124+
docker pull "$IMAGE"
125+
126+
- name: Run tests
127+
run: |
128+
IMAGE="${{ env.IMAGE_REGISTRY }}/${{ env.PACKAGE_REPOSITORY }}:${{ matrix.tag }}"
129+
make test-image IMAGE="$IMAGE" FLAVOR="${{ matrix.flavor }}"
130+
131+
results:
132+
# Skip for forked PRs since they won't have access to internal packages
133+
# Also use always() to ensure results are shown even if test job fails
134+
if: github.event.pull_request.head.repo.full_name == github.repository && always()
135+
needs: [setup, test]
136+
runs-on: ubuntu-latest
137+
steps:
138+
- name: Output
139+
run: |
140+
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
141+
echo "" >> $GITHUB_STEP_SUMMARY
142+
echo "| Input | Value |" >> $GITHUB_STEP_SUMMARY
143+
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
144+
echo "| Package Repository | ${{ env.PACKAGE_REPOSITORY }} |" >> $GITHUB_STEP_SUMMARY
145+
echo "| Tags | ${{ needs.setup.outputs.tags }} |" >> $GITHUB_STEP_SUMMARY
146+
echo "| Architectures | x86,arm |" >> $GITHUB_STEP_SUMMARY
147+
echo "" >> $GITHUB_STEP_SUMMARY
148+
149+
if [[ "${{ needs.test.result }}" == "success" ]]; then
150+
echo "✅ **All tests passed!**" >> $GITHUB_STEP_SUMMARY
151+
else
152+
echo "❌ **Some tests failed.** Check the job logs for details." >> $GITHUB_STEP_SUMMARY
153+
fi

.github/workflows/test_images.yaml

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ on:
1616
type: string
1717
required: true
1818
architectures:
19-
description: "Comma-separated list of architectures to test (amd64,arm64)"
19+
description: "Comma-separated list of architectures to test (x86,arm)"
2020
type: string
21-
default: "amd64,arm64"
21+
default: "x86,arm"
2222
required: false
2323

2424
permissions:
@@ -62,17 +62,24 @@ jobs:
6262
for arch in "${ARCHS[@]}"; do
6363
arch=$(echo "$arch" | xargs) # trim whitespace
6464
65-
# Determine runner based on architecture
66-
if [[ "$arch" == "arm64" ]]; then
67-
runner="ubuntu-24.04-arm64"
68-
else
65+
# Map user-friendly arch names to runner names
66+
# Accept "arm" or "arm64" -> use ubuntu-24.04-arm
67+
# Accept "x86" or "amd64" -> use ubuntu-latest
68+
if [[ "$arch" == "arm" ]] || [[ "$arch" == "arm64" ]]; then
69+
runner="ubuntu-24.04-arm"
70+
arch_display="arm"
71+
elif [[ "$arch" == "x86" ]] || [[ "$arch" == "amd64" ]]; then
6972
runner="ubuntu-latest"
73+
arch_display="x86"
74+
else
75+
echo "Error: Unknown architecture '$arch'. Use 'x86' or 'arm'"
76+
exit 1
7077
fi
7178
7279
if [[ -n "$matrix_items" ]]; then
7380
matrix_items+=","
7481
fi
75-
matrix_items+="{\"tag\":\"$tag\",\"arch\":\"$arch\",\"flavor\":\"$flavor\",\"runner\":\"$runner\"}"
82+
matrix_items+="{\"tag\":\"$tag\",\"arch\":\"$arch_display\",\"flavor\":\"$flavor\",\"runner\":\"$runner\"}"
7683
done
7784
done
7885
@@ -92,16 +99,9 @@ jobs:
9299
- name: Set up Go
93100
uses: actions/setup-go@v5
94101
with:
95-
go-version: '1.24.11'
102+
go-version: '1.23'
96103
cache-dependency-path: tests/go.sum
97104

98-
- name: Login to GitHub Container Registry
99-
env:
100-
GH_USER: ${{ github.actor }}
101-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
102-
run: |
103-
echo "${GH_TOKEN}" | docker login ghcr.io -u "${GH_USER}" --password-stdin
104-
105105
- name: Pull image
106106
run: |
107107
IMAGE="${{ env.IMAGE_REGISTRY }}/${{ inputs.package_repository }}:${{ matrix.tag }}"
@@ -113,15 +113,13 @@ jobs:
113113
IMAGE="${{ env.IMAGE_REGISTRY }}/${{ inputs.package_repository }}:${{ matrix.tag }}"
114114
make test-image IMAGE="$IMAGE" FLAVOR="${{ matrix.flavor }}"
115115
116-
summary:
116+
results:
117117
needs: [setup, test]
118118
runs-on: ubuntu-latest
119119
if: always()
120120
steps:
121-
- name: Test Summary
121+
- name: Output
122122
run: |
123-
echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
124-
echo "" >> $GITHUB_STEP_SUMMARY
125123
echo "| Input | Value |" >> $GITHUB_STEP_SUMMARY
126124
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
127125
echo "| Package Repository | ${{ inputs.package_repository }} |" >> $GITHUB_STEP_SUMMARY

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,6 @@ ifndef FLAVOR
6868
endif
6969
cd tests && go run main.go -image $(IMAGE) -flavor $(FLAVOR)
7070

71+
.PHONY: latest-tags
72+
latest-tags:
73+
@PGEDGE_LIST_LATEST_TAGS=1 ./scripts/build_pgedge_images.py

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,60 @@ volumes:
112112
- Mutable tags also exist for:
113113
- 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`
114114
- The latest image for a given Postgres major + spock major version, `pg<postgres major>-spock<major>-<flavor>`, e.g. `17-spock5-standard`
115+
116+
## Testing
117+
118+
This repository includes a comprehensive test suite to validate Postgres images. The tests verify:
119+
- Default entrypoint functionality
120+
- Patroni entrypoint (standard images only)
121+
- PostgreSQL connectivity and version checks
122+
- Extension availability and functionality (Spock, LOLOR, Snowflake, pgvector, PostGIS, pgaudit)
123+
- pgBackRest installation (standard images only)
124+
125+
### Running Tests Locally
126+
127+
To run tests locally, you'll need:
128+
- Go 1.24.11 or later
129+
- Docker installed and running
130+
- Access to pull the image you want to test
131+
132+
Run the test suite using the Makefile:
133+
134+
```bash
135+
make test-image IMAGE=<image> FLAVOR=<minimal|standard>
136+
```
137+
138+
Example:
139+
140+
```bash
141+
make test-image IMAGE=ghcr.io/pgedge/pgedge-postgres:17-spock5-standard FLAVOR=standard
142+
```
143+
144+
Or run directly with Go:
145+
146+
```bash
147+
cd tests && go run main.go -image <image> -flavor <minimal|standard>
148+
```
149+
150+
### Local Testing Limitations
151+
152+
**Architecture Limitations:** When running tests locally, you can only test images that match your local machine's architecture. For example:
153+
- On an x86_64/amd64 machine, you can only test amd64 images
154+
- On an ARM64 machine, you can only test arm64 images
155+
156+
To test images for multiple architectures, use the GitHub Actions workflow which runs tests on architecture-specific runners:
157+
- `ubuntu-latest` runner (amd64/x86_64 architecture)
158+
- `ubuntu-24.04-arm` runner (arm64 architecture)
159+
160+
### CI/CD Testing
161+
162+
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:
163+
- **amd64/x86_64**: Uses `ubuntu-latest` runner
164+
- **arm64**: Uses `ubuntu-24.04-arm` runner
165+
166+
The workflow accepts:
167+
- **Package Repository**: The container registry repository name
168+
- **Tags**: Comma-separated list of image tags to test
169+
- **Architectures**: Comma-separated list of architectures (`x86,arm` or `amd64,arm64`)
170+
171+
The workflow will automatically test each tag on each specified architecture by mapping the architecture names to the appropriate runner labels.

docker-entrypoint.sh

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,41 @@ docker_init_database_dir() {
114114
fi
115115

116116
# --pwfile refuses to handle a properly-empty file (hence the "\n"): https://github.com/docker-library/postgres/issues/1025
117-
eval 'initdb --username="$POSTGRES_USER" --pwfile=<(printf "%s\n" "$POSTGRES_PASSWORD") '"$POSTGRES_INITDB_ARGS"' "$@"'
117+
# Create a temporary password file to avoid command injection via POSTGRES_INITDB_ARGS
118+
local pwfile
119+
pwfile="$(mktemp)"
120+
# Ensure cleanup on exit (including errors) to prevent password file from being left on disk
121+
trap 'rm -f "$pwfile"' EXIT
122+
printf '%s\n' "$POSTGRES_PASSWORD" > "$pwfile"
123+
124+
# Build initdb command arguments safely using an array
125+
local initdb_args=(
126+
--username="$POSTGRES_USER"
127+
--pwfile="$pwfile"
128+
)
129+
130+
# Safely parse POSTGRES_INITDB_ARGS if it exists
131+
# Use read -a to split the string into an array without shell interpretation
132+
# This prevents command injection while preserving argument structure
133+
if [ -n "${POSTGRES_INITDB_ARGS:-}" ]; then
134+
# Read the arguments into an array, splitting on whitespace
135+
# Note: This does not handle quoted arguments with spaces.
136+
# For complex cases, pass arguments as function parameters instead.
137+
local args_array
138+
IFS=' ' read -r -a args_array <<< "$POSTGRES_INITDB_ARGS"
139+
initdb_args+=("${args_array[@]}")
140+
fi
141+
142+
# Add any function arguments (these are already safely parsed by the shell)
143+
initdb_args+=("$@")
144+
145+
# Execute initdb with the safely constructed arguments
146+
initdb "${initdb_args[@]}"
147+
148+
# Clean up temporary password file (trap will also handle this on exit, but explicit cleanup is good)
149+
rm -f "$pwfile"
150+
# Remove the trap since we've cleaned up manually
151+
trap - EXIT
118152

119153
# unset/cleanup "nss_wrapper" bits
120154
if [[ "${LD_PRELOAD:-}" == */libnss_wrapper.so ]]; then

scripts/build_pgedge_images.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Config:
1717
only_postgres_version: str
1818
only_spock_version: str
1919
only_arch: str
20+
list_latest_tags: bool
2021

2122
@staticmethod
2223
def from_env() -> "Config":
@@ -29,6 +30,7 @@ def from_env() -> "Config":
2930
only_postgres_version=os.getenv("PGEDGE_IMAGE_ONLY_POSTGRES_VERSION", ""),
3031
only_spock_version=os.getenv("PGEDGE_IMAGE_ONLY_SPOCK_VERSION", ""),
3132
only_arch=os.getenv("PGEDGE_IMAGE_ONLY_ARCH", ""),
33+
list_latest_tags=(os.getenv("PGEDGE_LIST_LATEST_TAGS", "0") == "1"),
3234
)
3335

3436

@@ -324,6 +326,13 @@ def main():
324326

325327
config = Config.from_env()
326328

329+
# If list_latest_tags is enabled, output tags and exit
330+
if config.list_latest_tags:
331+
tags = get_latest_tags()
332+
# Output as comma-separated list
333+
print(",".join(tags))
334+
return
335+
327336
if config.dry_run:
328337
logging.info("dry run enabled. build and publish actions will be skipped.")
329338

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

399408

409+
def get_latest_tags() -> list[str]:
410+
"""
411+
Returns a list of the latest immutable tags (with epoch and pg minor version)
412+
for all images that are marked as latest for their Postgres major version.
413+
Returns tags in the format: {postgres_version}-spock{spock_version}-{flavor}-{epoch}
414+
"""
415+
latest_tags = []
416+
for image in all_images:
417+
if image.is_latest_for_pg_major:
418+
# Get the immutable build tag with epoch and full postgres version
419+
latest_tags.append(str(image.build_tag))
420+
return latest_tags
421+
422+
400423
if __name__ == "__main__":
401424
main()

0 commit comments

Comments
 (0)