intermediate commit
Build and Push Docker Images / build-and-push (push) Failing after 37s

This commit is contained in:
2025-11-17 21:56:21 +01:00
parent b516c942df
commit d72ee4a7d2
8 changed files with 667 additions and 13 deletions
+16
View File
@@ -0,0 +1,16 @@
FROM python:3.12-slim
# Set workdir
WORKDIR /app
# Copy requirements first (better caching)
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy source code
COPY . .
# Run script
CMD ["python", "main.py"]
+112
View File
@@ -0,0 +1,112 @@
# Docker Webhook Updater
A Docker container that monitors a Docker image for updates and triggers a webhook when a new version is detected.
## Features
- Monitors Docker images from any registry (Docker Hub, GHCR, Quay.io, custom registries)
- Detects image updates by comparing SHA256 digests
- Triggers webhook with bearer token authentication
- Persistent SHA storage across container restarts
- Configurable check interval
## Environment Variables
| Variable | Description | Required | Example |
| ----------------- | --------------------------------------- | -------- | ----------------------------------------------------- |
| `DOCKER_REPO_URL` | Docker registry URL | Yes | `docker.io`, `ghcr.io`, `quay.io`, or custom registry |
| `DOCKER_IMAGE` | Full image name with tag | Yes | `username/image:latest`, `library/nginx:1.25` |
| `WEBHOOK_URL` | URL to call when image is updated | Yes | `https://example.com/webhook` |
| `WEBHOOK_TOKEN` | Bearer token for webhook authentication | Yes | `your-secret-token` |
| `CHECK_INTERVAL` | Seconds between checks (default: 300) | No | `600` |
## Usage
### Docker Hub Example
```bash
docker run -d \
-e DOCKER_REPO_URL=docker.io \
-e DOCKER_IMAGE=library/nginx:latest \
-e WEBHOOK_URL=https://example.com/webhook \
-e WEBHOOK_TOKEN=your-secret-token \
-e CHECK_INTERVAL=300 \
your-registry/docker-webhook-updater:latest
```
### GitHub Container Registry Example
```bash
docker run -d \
-e DOCKER_REPO_URL=ghcr.io \
-e DOCKER_IMAGE=username/repo:latest \
-e WEBHOOK_URL=https://example.com/webhook \
-e WEBHOOK_TOKEN=your-secret-token \
your-registry/docker-webhook-updater:latest
```
### Docker Compose Example
```yaml
version: "3.8"
services:
webhook-updater:
image: your-registry/docker-webhook-updater:latest
environment:
- DOCKER_REPO_URL=docker.io
- DOCKER_IMAGE=library/nginx:latest
- WEBHOOK_URL=https://example.com/webhook
- WEBHOOK_TOKEN=your-secret-token
- CHECK_INTERVAL=300
restart: unless-stopped
```
## How It Works
1. The service periodically checks the Docker registry for the specified image
2. It retrieves the SHA256 digest of the image manifest
3. Compares the current SHA with the previously stored SHA
4. If different, triggers the webhook with a POST request containing:
```json
{
"event": "image_updated",
"image": "username/image:latest",
"repo_url": "docker.io"
}
```
5. Updates the stored SHA only after successful webhook trigger
## Webhook Request Format
When an image update is detected, the service sends a POST request to the webhook URL:
- **Method**: POST
- **Headers**:
- `Authorization: Bearer {WEBHOOK_TOKEN}`
- `Content-Type: application/json`
- **Body**:
```json
{
"event": "image_updated",
"image": "username/image:tag",
"repo_url": "docker.io"
}
```
## Notes
- The SHA is stored in `/app/sha.txt` inside the container
- On first run, the SHA is stored without triggering the webhook
- If the webhook fails, the SHA is not updated, so it will retry on the next check
- For private Docker images, authentication may be required (not currently supported)
- The service runs continuously and checks at the specified interval
## Building
```bash
docker build -t docker-webhook-updater:latest .
```
+257
View File
@@ -0,0 +1,257 @@
import os
import sys
import time
import logging
import requests
from typing import Optional
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
# Environment variables
DOCKER_REPO_URL = os.getenv('DOCKER_REPO_URL')
DOCKER_IMAGE = os.getenv('DOCKER_IMAGE')
WEBHOOK_URL = os.getenv('WEBHOOK_URL')
WEBHOOK_TOKEN = os.getenv('WEBHOOK_TOKEN')
# SHA storage file
SHA_FILE = '/app/sha.txt'
# Check interval in seconds (default: 300 = 5 minutes)
CHECK_INTERVAL = int(os.getenv('CHECK_INTERVAL', '300'))
def validate_env_vars():
"""Validate that all required environment variables are set."""
missing = []
if not DOCKER_REPO_URL:
missing.append('DOCKER_REPO_URL')
if not DOCKER_IMAGE:
missing.append('DOCKER_IMAGE')
if not WEBHOOK_URL:
missing.append('WEBHOOK_URL')
if not WEBHOOK_TOKEN:
missing.append('WEBHOOK_TOKEN')
if missing:
logger.error(f"Missing required environment variables: {', '.join(missing)}")
sys.exit(1)
def get_image_sha(repo_url: str, image: str) -> Optional[str]:
"""
Get the SHA256 digest of the Docker image from the registry.
Args:
repo_url: Docker registry URL (e.g., docker.io, ghcr.io, or custom registry)
image: Full image name with tag (e.g., 'username/image:tag' or 'username/image:latest')
Returns:
SHA256 digest string or None if failed
"""
try:
# Parse image name and tag
if ':' in image:
image_name, tag = image.rsplit(':', 1)
else:
image_name = image
tag = 'latest'
# Normalize repo URL (remove https://, http://, trailing /)
repo_url_normalized = repo_url.rstrip('/').replace('https://', '').replace('http://', '')
# Construct manifest URL
# For Docker Hub: https://registry-1.docker.io/v2/{namespace}/{image}/manifests/{tag}
# For other registries: https://{repo_url}/v2/{image}/manifests/{tag}
if repo_url_normalized in ('docker.io', 'hub.docker.com', ''):
# Docker Hub uses registry-1.docker.io
registry_url = 'https://registry-1.docker.io'
# For Docker Hub, image_name should be 'username/image' or 'library/image' for official images
# If no namespace, assume it's an official image (library namespace)
if '/' not in image_name:
image_name = f'library/{image_name}'
manifest_url = f"{registry_url}/v2/{image_name}/manifests/{tag}"
else:
# Other registries (ghcr.io, quay.io, custom registries)
registry_url = f"https://{repo_url_normalized}"
manifest_url = f"{registry_url}/v2/{image_name}/manifests/{tag}"
logger.debug(f"Fetching manifest from: {manifest_url}")
# Request manifest with Accept header for schema v2
headers = {
'Accept': 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json'
}
response = requests.get(manifest_url, headers=headers, timeout=30)
response.raise_for_status()
# Get SHA from Docker-Content-Digest header (preferred)
sha = response.headers.get('Docker-Content-Digest')
if not sha:
# Fallback: try to get from response if it's a manifest list
try:
manifest = response.json()
# For manifest lists, get the digest from the first platform-specific manifest
if manifest.get('mediaType') == 'application/vnd.docker.distribution.manifest.list.v2+json':
if 'manifests' in manifest and len(manifest['manifests']) > 0:
sha = manifest['manifests'][0].get('digest')
elif 'config' in manifest and 'digest' in manifest['config']:
sha = manifest['config']['digest']
elif 'digest' in manifest:
sha = manifest['digest']
except Exception as e:
logger.debug(f"Could not parse manifest JSON: {e}")
if sha:
logger.info(f"Current SHA for {image}: {sha[:16]}...")
return sha
else:
logger.warning(f"Could not extract SHA from manifest response")
return None
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
logger.error(f"Image not found: {image} (tag: {tag})")
elif e.response.status_code == 401:
logger.error(f"Authentication required for {repo_url}. Private images may require authentication.")
else:
logger.error(f"HTTP error fetching image SHA: {e}")
return None
except requests.exceptions.RequestException as e:
logger.error(f"Failed to fetch image SHA: {e}")
return None
except Exception as e:
logger.error(f"Unexpected error fetching image SHA: {e}")
return None
def load_stored_sha() -> Optional[str]:
"""Load the previously stored SHA from file."""
try:
if os.path.exists(SHA_FILE):
with open(SHA_FILE, 'r') as f:
sha = f.read().strip()
if sha:
logger.debug(f"Loaded stored SHA: {sha}")
return sha
except Exception as e:
logger.warning(f"Failed to load stored SHA: {e}")
return None
def save_sha(sha: str):
"""Save the SHA to file."""
try:
with open(SHA_FILE, 'w') as f:
f.write(sha)
logger.debug(f"Saved SHA: {sha}")
except Exception as e:
logger.error(f"Failed to save SHA: {e}")
def trigger_webhook(url: str, token: str) -> bool:
"""
Trigger the webhook with bearer token authentication.
Args:
url: Webhook URL
token: Bearer token
Returns:
True if successful, False otherwise
"""
try:
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
# Send POST request (you can modify payload if needed)
payload = {
'event': 'image_updated',
'image': DOCKER_IMAGE,
'repo_url': DOCKER_REPO_URL
}
logger.info(f"Triggering webhook: {url}")
response = requests.post(url, json=payload, headers=headers, timeout=30)
response.raise_for_status()
logger.info(f"Webhook triggered successfully (status: {response.status_code})")
return True
except requests.exceptions.RequestException as e:
logger.error(f"Failed to trigger webhook: {e}")
return False
except Exception as e:
logger.error(f"Unexpected error triggering webhook: {e}")
return False
def main():
"""Main loop to check for image updates."""
logger.info("Starting Docker Webhook Updater")
logger.info(f"Monitoring: {DOCKER_REPO_URL}/{DOCKER_IMAGE}")
logger.info(f"Webhook URL: {WEBHOOK_URL}")
logger.info(f"Check interval: {CHECK_INTERVAL} seconds")
validate_env_vars()
# Load initial SHA
stored_sha = load_stored_sha()
if stored_sha:
logger.info(f"Initial stored SHA: {stored_sha}")
else:
logger.info("No stored SHA found. Will trigger webhook on first check if image exists.")
while True:
try:
# Get current SHA from registry
current_sha = get_image_sha(DOCKER_REPO_URL, DOCKER_IMAGE)
if current_sha:
# Compare with stored SHA
if stored_sha and current_sha != stored_sha:
logger.info(f"Image updated! Old SHA: {stored_sha[:16]}..., New SHA: {current_sha[:16]}...")
# Trigger webhook
if trigger_webhook(WEBHOOK_URL, WEBHOOK_TOKEN):
# Save new SHA only if webhook was successful
save_sha(current_sha)
stored_sha = current_sha
else:
logger.warning("Webhook failed. SHA not updated. Will retry on next check.")
elif not stored_sha:
# First run - just store the SHA without triggering webhook
logger.info(f"First run: storing initial SHA: {current_sha[:16]}...")
save_sha(current_sha)
stored_sha = current_sha
else:
logger.debug(f"No update detected. SHA: {current_sha[:16]}...")
else:
logger.warning("Could not fetch image SHA. Will retry on next check.")
# Wait before next check
logger.debug(f"Waiting {CHECK_INTERVAL} seconds before next check...")
time.sleep(CHECK_INTERVAL)
except KeyboardInterrupt:
logger.info("Received interrupt signal. Shutting down...")
break
except Exception as e:
logger.error(f"Unexpected error in main loop: {e}")
logger.info(f"Waiting {CHECK_INTERVAL} seconds before retry...")
time.sleep(CHECK_INTERVAL)
if __name__ == "__main__":
main()