Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .github/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PyGithub==2.1.1
ruamel.yaml==0.18.5
packaging==23.2
requests==2.31.0
117 changes: 43 additions & 74 deletions utils/update_images.py → .github/update_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

import os
import re
import json
import logging
import sys
import traceback
from datetime import datetime, timezone
from typing import Dict, List, Optional, Any
from pathlib import Path
from urllib.parse import urlparse
Expand All @@ -17,7 +15,7 @@


def setup_logging():
"""Set up structured logging with timestamps."""
"""Set up logging."""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
Expand All @@ -37,9 +35,15 @@ def __init__(self, github_token: str):
self.timeout = 30
self.branch_name = "update-docker-images"

# List of images not to update
self.ignored_images = [
"grafana/beyla"
]

self.values_file_path = Path("deploy/helm/values.yaml")
self.chart_file_path = Path("deploy/helm/Chart.yaml")

self.modified_files: List[str] = []

# YAML configuration
self.yaml = YAML()
Expand Down Expand Up @@ -91,13 +95,11 @@ def get_docker_hub_tags(self, repository: str, limit: int = 200) -> List[str]:
def get_ghcr_tags(self, repository: str) -> List[str]:
"""Fetch tags from GitHub Container Registry."""
try:
# Parse the repository string as a URL to properly extract the hostname
# Parse the repository string as a URL to extract the hostname
repo_url = repository if repository.startswith(('http://', 'https://')) else f'https://{repository}'
parsed_url = urlparse(repo_url)

# Check if the hostname is exactly 'ghcr.io'
if parsed_url.hostname == 'ghcr.io':
# Remove the leading slash and extract the repository path
repo_path = parsed_url.path.lstrip('/')
else:
repo_path = repository
Expand Down Expand Up @@ -152,7 +154,7 @@ def _get_ghcr_api_tags(self, owner: str, package_name: str) -> List[str]:
return []

def _get_github_release_tags(self, owner: str, repo_name: str) -> List[str]:
"""Get tags from GitHub releases as fallback."""
"""Get tags from GitHub releases."""
try:
releases_repo = self.github.get_repo(f"{owner}/{repo_name}")
releases = releases_repo.get_releases()
Expand All @@ -166,31 +168,20 @@ def _get_github_release_tags(self, owner: str, repo_name: str) -> List[str]:

def get_latest_version(self, repository: str, current_version: str = "") -> Optional[str]:
"""Get the latest semantic version for a Docker image."""
clean_repo = repository.strip()

# Parse the repository string as a URL to properly extract the hostname
repo_url = clean_repo if clean_repo.startswith(('http://', 'https://')) else f'https://{clean_repo}'
parsed_url = urlparse(repo_url)
hostname = parsed_url.hostname

# Handle Docker Hub aliases by normalizing them
if hostname in ['docker.io', 'index.docker.io']:
clean_repo = parsed_url.path.lstrip('/')
elif hostname == 'ghcr.io':
clean_repo = parsed_url.path.lstrip('/')

# Determine which registry API to use based on hostname
if hostname == 'ghcr.io':
repository = repository.strip()

repo_url = repository if repository.startswith(('http://', 'https://')) else f'https://{repository}'
parsed = urlparse(repo_url)
hostname = parsed.hostname or ""
is_ghcr = hostname == 'ghcr.io'

if hostname in ('docker.io', 'index.docker.io'):
repository = parsed.path.lstrip('/')

if is_ghcr:
tags = self.get_ghcr_tags(repository)
elif hostname in ['docker.io', 'index.docker.io', None]:
# None hostname means it's likely a Docker Hub repository without explicit hostname
tags = self.get_docker_hub_tags(clean_repo)
elif hostname in ['gcr.io', 'quay.io']:
# For other registries, fallback to Docker Hub API format (may not always work)
tags = self.get_docker_hub_tags(clean_repo)
else:
# Default fallback for unknown registries
tags = self.get_docker_hub_tags(clean_repo)
tags = self.get_docker_hub_tags(repository)

if not tags:
self.logger.warning(f"No tags found for {repository}")
Expand Down Expand Up @@ -275,6 +266,10 @@ def update_values_yaml(self) -> List[Dict[str, Any]]:
self.logger.debug(f"Skipping {repository} with placeholder tag: {current_tag}")
continue

if repository in self.ignored_images:
self.logger.info(f"Skipping ignored image: {repository}")
continue

self.logger.info(f"Checking {repository}:{current_tag}")
latest_tag = self.get_latest_version(repository, current_tag)

Expand All @@ -292,6 +287,7 @@ def update_values_yaml(self) -> List[Dict[str, Any]]:
if updates:
with open(self.values_file_path, 'w') as f:
self.yaml.dump(yaml_data, f)
self.modified_files.append(str(self.values_file_path))

return updates

Expand All @@ -310,7 +306,7 @@ def _bump_version(self, old_version: str) -> str:
# Handle standard semantic versions
version_parts = old_version.split('.')
if len(version_parts) >= 3:
patch_part = version_parts[2].split('-')[0] # Handle pre-release suffixes
patch_part = version_parts[2].split('-')[0]
if patch_part.isdigit():
new_patch = str(int(patch_part) + 1)
return f"{version_parts[0]}.{version_parts[1]}.{new_patch}"
Expand All @@ -321,7 +317,7 @@ def _bump_version(self, old_version: str) -> str:
return old_version # Return original if parsing fails

def update_chart_version(self, updates: List[Dict[str, Any]]) -> bool:
"""Update Chart.yaml version and appVersion minimally."""
"""Update Chart.yaml version and appVersion."""
if not updates or not self.chart_file_path.exists():
return False

Expand Down Expand Up @@ -367,6 +363,7 @@ def update_chart_version(self, updates: List[Dict[str, Any]]) -> bool:
if content != original_content:
with open(self.chart_file_path, 'w') as f:
f.write(content)
self.modified_files.append(str(self.chart_file_path))

return True

Expand All @@ -375,7 +372,7 @@ def update_chart_version(self, updates: List[Dict[str, Any]]) -> bool:
return False

def create_or_update_branch(self, updates: List[Dict[str, Any]]) -> bool:
"""Create or update the update branch with changes."""
"""Create or update the branch with changes."""
if not updates:
return False

Expand Down Expand Up @@ -416,39 +413,29 @@ def commit_changes(self, updates: List[Dict[str, Any]]) -> bool:
branch_ref = self.repo.get_git_ref(f"heads/{self.branch_name}")
base_commit = self.repo.get_git_commit(branch_ref.object.sha)

# Get current tree
current_tree = base_commit.tree

# Prepare updated files
tree_elements = []

if self.values_file_path.exists():
with open(self.values_file_path, 'r', encoding='utf-8') as f:
values_content = f.read()
tree_elements.append(InputGitTreeElement(
path=str(self.values_file_path),
mode='100644',
type='blob',
content=values_content
))

if self.chart_file_path.exists():
with open(self.chart_file_path, 'r', encoding='utf-8') as f:
chart_content = f.read()
for file_path in sorted(set(self.modified_files)):
if not Path(file_path).exists():
continue
with open(file_path, 'r', encoding='utf-8') as f:
file_content = f.read()
tree_elements.append(InputGitTreeElement(
path=str(self.chart_file_path),
path=file_path,
mode='100644',
type='blob',
content=chart_content
content=file_content
))

# Create new tree based on current tree

if not tree_elements:
self.logger.info("No file content changes detected to commit")
return True

current_tree = base_commit.tree
new_tree = self.repo.create_git_tree(tree_elements, base_tree=current_tree)

# Create commit
commit = self.repo.create_git_commit(commit_message, new_tree, [base_commit])

# Update branch reference
branch_ref.edit(sha=commit.sha)

self.logger.info(f"Committed changes to {self.branch_name}")
Expand Down Expand Up @@ -501,23 +488,6 @@ def create_or_update_pr(self, updates: List[Dict[str, Any]]) -> Optional[str]:
self.logger.error(f"Failed to create/update PR: {e}")
return None

def save_changes_log(self, updates: List[Dict[str, Any]]):
"""Save changes to JSON file for debugging."""
log_data = {
'timestamp': datetime.now(timezone.utc).isoformat(),
'updates': updates,
'summary': {
'total_updates': len(updates),
'repositories_updated': len(set(u['repository'] for u in updates))
}
}

filename = f"changes_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(filename, 'w') as f:
json.dump(log_data, f, indent=2)

self.logger.info(f"Saved changes log to {filename}")

def run(self) -> bool:
"""Main execution method."""
self.logger.info("Starting Docker Image Updater")
Expand All @@ -532,7 +502,6 @@ def run(self) -> bool:
self.logger.info(f"Found {len(updates)} image updates")

self.update_chart_version(updates)
self.save_changes_log(updates)

if not self.create_or_update_branch(updates):
return False
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/updateImages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r .github/scripts/requirements.txt
pip install -r .github/requirements.txt

- name: Configure Git
run: |
Expand All @@ -42,5 +42,5 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python .github/scripts/update_docker_images.py
python .github/update_images.py

3 changes: 2 additions & 1 deletion deploy/helm/tests/metrics-deployment_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ tests:
set:
otel.metrics.prometheus_check: true
otel.metrics.prometheus.url: "http://prometheus:9090"
otel.init_images.busy_box.tag: "1.0.0"
asserts:
- equal:
path: spec.template.spec.initContainers[0].image
value: busybox:1.36.1
value: busybox:1.0.0
17 changes: 9 additions & 8 deletions deploy/helm/tests/swo-agent-statefulset_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ suite: Test for swo-agent-statefulset
templates:
- swo-agent-statefulset.yaml
tests:
- it: Image should be correct in default state
template: swo-agent-statefulset.yaml
set:
swoagent.enabled: true
asserts:
- equal:
path: spec.template.spec.containers[0].image
value: solarwinds/swo-agent:v2.10.318
- it: Image should be correct when overriden tag
template: swo-agent-statefulset.yaml
set:
Expand All @@ -32,3 +24,12 @@ tests:
- equal:
path: spec.template.spec.containers[0].image
value: azurek8s.azure.io/marketplaceimages/swo-agent:v1.2.3@abcd
- it: Image should be correct in default state
template: swo-agent-statefulset.yaml
set:
swoagent.image.tag: "v1.0.0"
swoagent.enabled: true
asserts:
- equal:
path: spec.template.spec.containers[0].image
value: solarwinds/swo-agent:v1.0.0
Loading