diff --git a/app/pyproject.toml b/app/pyproject.toml index 1deff3ce..01406142 100644 --- a/app/pyproject.toml +++ b/app/pyproject.toml @@ -3,7 +3,7 @@ [project] name = "github-runner-image-builder" -version = "0.13.0" +version = "0.14.0" authors = [ { name = "Canonical IS DevOps", email = "is-devops-team@canonical.com" }, ] diff --git a/app/src/github_runner_image_builder/templates/cloud-init.sh.j2 b/app/src/github_runner_image_builder/templates/cloud-init.sh.j2 index 2b4ebcfa..6c399299 100644 --- a/app/src/github_runner_image_builder/templates/cloud-init.sh.j2 +++ b/app/src/github_runner_image_builder/templates/cloud-init.sh.j2 @@ -66,6 +66,8 @@ function install_apt_packages() { echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list fi + echo "Adding dotnet backports PPA" + DEBIAN_FRONTEND=noninteractive /usr/bin/add-apt-repository -y ppa:dotnet/backports echo "Updating apt packages" DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get update -y echo "Upgrading apt packages" diff --git a/app/tests/unit/test_openstack_builder.py b/app/tests/unit/test_openstack_builder.py index 97350053..57594415 100644 --- a/app/tests/unit/test_openstack_builder.py +++ b/app/tests/unit/test_openstack_builder.py @@ -777,6 +777,8 @@ def test__generate_cloud_init_script( main" > /etc/apt/sources.list.d/github-cli.list fi + echo "Adding dotnet backports PPA" + DEBIAN_FRONTEND=noninteractive /usr/bin/add-apt-repository -y ppa:dotnet/backports echo "Updating apt packages" DEBIAN_FRONTEND=noninteractive /usr/bin/apt-get update -y echo "Upgrading apt packages" diff --git a/docs/changelog.md b/docs/changelog.md index ef6c8a0c..0c85acc9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ +## [#222 Add PPA for .NET backport during image building](https://github.com/canonical/github-runner-image-builder-operator/pull/222) (2026-05-22) + +- Add .NET PPA in image building. This allows wider range of .NET version to be installed. + ## [#219 Use Juju secrets](https://github.com/canonical/github-runner-image-builder-operator/pull/219) (2026-04-17) - Require `openstack-password-secret` configuration option to securely store OpenStack passwords using Juju secrets. diff --git a/lib/charms/grafana_agent/v0/cos_agent.py b/lib/charms/grafana_agent/v0/cos_agent.py index 228550af..d3944207 100644 --- a/lib/charms/grafana_agent/v0/cos_agent.py +++ b/lib/charms/grafana_agent/v0/cos_agent.py @@ -211,7 +211,9 @@ def __init__(self, *args): ``` """ +import copy import enum +import hashlib import json import logging import socket @@ -254,7 +256,7 @@ class _MetricsEndpointDict(TypedDict): LIBID = "dc15fa84cef84ce58155fb84f6c6213a" LIBAPI = 0 -LIBPATCH = 24 +LIBPATCH = 25 PYDEPS = ["cosl >= 0.0.50", "pydantic"] @@ -308,6 +310,13 @@ def _dedupe_list(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]: return unique_items +def _dict_hash_except_key(scrape_config: Dict[str, Any], key: Optional[str]): + """Get a hash of the scrape_config dict, except for the specified key.""" + cfg_for_hash = {k: v for k, v in scrape_config.items() if k != key} + serialized = json.dumps(cfg_for_hash, sort_keys=True) + return hashlib.blake2b(serialized.encode(), digest_size=4).hexdigest() + + class TracingError(Exception): """Base class for custom errors raised by tracing.""" @@ -697,6 +706,27 @@ def _on_refresh(self, event): ) as e: logger.error("Invalid relation data provided: %s", e) + def _deterministic_scrape_configs( + self, scrape_configs: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Get deterministic scrape_configs with stable job names. + + For stability across serializations, compute a short per-config hash + and append it to the existing job name (or 'default'). Keep the app + name as a prefix: __<8hex-hash>. + + Hash the whole scrape_config (except any existing job_name) so the + suffix is sensitive to all stable fields. Use deterministic JSON + serialization. + """ + local_scrape_configs = copy.deepcopy(scrape_configs) + for scrape_config in local_scrape_configs: + name = scrape_config.get("job_name", "default") + short_id = _dict_hash_except_key(scrape_config, "job_name") + scrape_config["job_name"] = f"{self._charm.app.name}_{name}_{short_id}" + + return sorted(local_scrape_configs, key=lambda c: c.get("job_name", "")) + @property def _scrape_jobs(self) -> List[Dict]: """Return a list of scrape_configs. @@ -711,22 +741,17 @@ def _scrape_jobs(self) -> List[Dict]: scrape_configs = self._scrape_configs.copy() # Convert "metrics_endpoints" to standard scrape_configs, and add them in - unit_name = self._charm.unit.name.replace("/", "_") for endpoint in self._metrics_endpoints: - port = endpoint["port"] - path = endpoint["path"] - sanitized_path = path.strip("/").replace("/", "_") scrape_configs.append( { - "job_name": f"{unit_name}_localhost_{port}_{sanitized_path}", - "metrics_path": path, - "static_configs": [{"targets": [f"localhost:{port}"]}], + "metrics_path": endpoint["path"], + "static_configs": [{"targets": [f"localhost:{endpoint['port']}"]}], } ) scrape_configs = scrape_configs or [] - return scrape_configs + return self._deterministic_scrape_configs(scrape_configs) @property def _metrics_alert_rules(self) -> Dict: @@ -742,7 +767,7 @@ def _metrics_alert_rules(self) -> Dict: ) alert_rules.add_path(self._metrics_rules, recursive=self._recursive) alert_rules.add( - generic_alert_groups.application_rules, + copy.deepcopy(generic_alert_groups.application_rules), group_name_prefix=JujuTopology.from_charm(self._charm).identifier, )