Skip to content

Mirror External Images #666

Mirror External Images

Mirror External Images #666

name: Mirror External Images
# SECURITY NOTES:
# 1. This workflow handles sensitive secrets (registry credentials, private keys)
# 2. Command injection mitigations:
# - All untrusted inputs (branch names, etc.) are passed via environment variables
# - Never use ${{ }} syntax directly in bash contexts with untrusted data
# 3. workflow_run validation:
# - Only accepts triggers from this repository (not forks)
# - Validates repository source before processing
# 4. Minimal GITHUB_TOKEN permissions (permissions: {})
# 5. If secrets are compromised, rotate immediately:
# - TOOLS_REPO_READER_PRIVATE_KEY
# - REGISTRY_REDHAT_IO_RGY_PASSWORD
# - REGISTRY_STAGE_REDHAT_IO_RGY_PASSWORD
# - QUAY_IO_ACMD_RGY_PASSWORD
# - SLACK_WEBHOOK_URL
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to checkout (defaults to current branch)'
required: false
type: string
dry-run:
description: 'Run in dry-run mode (no actual mirroring)'
required: false
type: boolean
default: false
workflow_run:
workflows: ["Gen Bundle Contents When Triggered"]
types:
- completed
defaults:
run:
shell: bash
# Restrict GITHUB_TOKEN permissions to minimum required
# This prevents token misuse if the workflow is compromised
permissions: {}
jobs:
mirror-images:
runs-on: ubuntu-24.04
# Only run if the triggering workflow succeeded or if manually triggered
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Validate workflow_run repository
if: github.event_name == 'workflow_run'
env:
WORKFLOW_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
EXPECTED_REPO: ${{ github.repository }}
run: |
echo "Validating workflow_run trigger source..."
echo "Expected repository: ${EXPECTED_REPO}"
echo "Triggering repository: ${WORKFLOW_REPO}"
if [ "${WORKFLOW_REPO}" != "${EXPECTED_REPO}" ]; then
echo "ERROR: workflow_run triggered from untrusted repository!"
echo "This workflow only accepts triggers from ${EXPECTED_REPO}"
exit 1
fi
echo "Repository validation passed."
- name: Log workflow trigger context
env:
EVENT_NAME: ${{ github.event_name }}
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
WORKFLOW_SHA: ${{ github.event.workflow_run.head_sha }}
WORKFLOW_BRANCH: ${{ github.event.workflow_run.head_branch }}
WORKFLOW_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
INPUT_BRANCH: ${{ inputs.branch }}
run: |
echo "Workflow triggered by: ${EVENT_NAME}"
if [ "${EVENT_NAME}" = "workflow_run" ]; then
echo "Triggering workflow: ${WORKFLOW_NAME}"
echo "Triggering commit SHA: ${WORKFLOW_SHA}"
echo "Triggering branch: ${WORKFLOW_BRANCH}"
echo "Triggering repo: ${WORKFLOW_REPO}"
elif [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
echo "Manual trigger - branch input: ${INPUT_BRANCH}"
fi
- name: Checkout repository
uses: actions/checkout@v4
with:
# When triggered by workflow_run, checkout the commit SHA that triggered it (persists even if branch is deleted)
# When triggered manually, checkout the specified branch or current branch
ref: ${{ inputs.branch || github.event.workflow_run.head_sha || github.ref }}
path: workflow
sparse-checkout: |
config
show-progress: false
- name: Generate GitHub token to read release tools repo
id: gen-tools-repo-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.TOOLS_REPO_READER_APP_ID }}
private-key: ${{ secrets.TOOLS_REPO_READER_PRIVATE_KEY }}
owner: ${{ vars.TOOLS_REPO_OWNER }}
repositories: "release"
- name: Checkout release tools repo
uses: actions/checkout@v4
with:
repository: ${{ vars.TOOLS_REPO_OWNER }}/release
ref: master
token: ${{ steps.gen-tools-repo-token.outputs.token }}
path: release-tools
sparse-checkout: |
tools/konflux/bundle/utils
show-progress: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install skopeo
run: |
sudo apt-get update
sudo apt-get install -y skopeo
- name: Authenticate and run mirror script
id: mirror
continue-on-error: true
run: |
# Authenticate to needed image registries...
# Access to registry.redhat.io needed to resolve external image references (shipped images):
echo "Doing skopeo login to registry.redhat.io"
echo "${{ secrets.REGISTRY_REDHAT_IO_RGY_PASSWORD }}" \
| skopeo login -u="${{ vars.REGISTRY_REDHAT_IO_RGY_USERNAME }}" --password-stdin registry.redhat.io
# Access to registry.stage.redhat.io needed to resolve staging image references:
echo "Doing skopeo login to registry.stage.redhat.io"
echo "${{ secrets.REGISTRY_STAGE_REDHAT_IO_RGY_PASSWORD }}" \
| skopeo login -u="${{ vars.REGISTRY_STAGE_REDHAT_IO_RGY_USERNAME }}" --password-stdin registry.stage.redhat.io
# Access to quay.io/acm-d needed to resolve external image references (in-dev images):
echo "Doing skopeo login to quay.io (for acm-d)"
echo "${{ secrets.QUAY_IO_ACMD_RGY_PASSWORD }}" \
| skopeo login -u="${{ vars.QUAY_IO_ACMD_RGY_USERNAME }}" --password-stdin quay.io
# Run the mirror script
ARGS="--operator mce"
if [ "${{ inputs.dry-run }}" == "true" ]; then
ARGS="$ARGS --dry-run"
fi
# Run the script from stolostron/release repo, using config from workflow (bundle) repo
cd workflow
python ../release-tools/tools/konflux/bundle/utils/mirror-external-images.py $ARGS
- name: Send Slack notification
if: always()
env:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
run: |
# Read results from the JSON file
if [ ! -f workflow/mirror-results.json ]; then
echo "No results file found, creating minimal report"
STATUS="❌ *FAILED*"
SUMMARY="Mirror script did not complete successfully"
DETAILS=""
else
# Parse the JSON results
MIRRORED=$(jq -r '.mirrored' workflow/mirror-results.json)
SKIPPED=$(jq -r '.skipped' workflow/mirror-results.json)
FAILED=$(jq -r '.failed' workflow/mirror-results.json)
DRY_RUN=$(jq -r '.dry_run' workflow/mirror-results.json)
# Determine status
if [ "$FAILED" -gt 0 ]; then
STATUS="⚠️ *COMPLETED WITH FAILURES*"
elif [ "$DRY_RUN" = "true" ]; then
STATUS="🔍 *DRY RUN COMPLETED*"
else
STATUS="✅ *SUCCESS*"
fi
# Build summary
SUMMARY="*Summary:*\n• Mirrored: $MIRRORED\n• Skipped: $SKIPPED\n• Failed: $FAILED"
# Build failure details if any
DETAILS=""
if [ "$FAILED" -gt 0 ]; then
DETAILS="\n\n*Failed Images:*\n"
FAILURES=$(jq -r '.failures[] | "• `\(.image_key)` [\(.operator)]\n Source: `\(.source)`\n Target: `\(.target)`\n Error: \(.error)\n"' workflow/mirror-results.json)
DETAILS="$DETAILS$FAILURES"
fi
fi
# Build the Slack message
WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
MESSAGE=$(cat <<EOF
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Image Mirroring Report"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "$STATUS\n\n$SUMMARY$DETAILS"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Triggered by:*\n${EVENT_NAME}"
},
{
"type": "mrkdwn",
"text": "*Branch:*\n${REF_NAME}"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Workflow Run"
},
"url": "$WORKFLOW_URL"
}
]
}
]
}
EOF
)
# Send to Slack
curl -X POST -H 'Content-type: application/json' \
--data "$MESSAGE" \
"${{ secrets.SLACK_WEBHOOK_URL }}"
- name: Fail workflow if mirroring failed
if: steps.mirror.outcome == 'failure'
run: exit 1