mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Merge branch 'main' into feature/store-i18n
# Conflicts: # roles/update/README.md # roles/web-app-bluesky/templates/extra_locations.conf.j2 # roles/web-app-opentalk/templates/nginx/extra.conf.j2 # scripts/install/filesystem.sh
This commit is contained in:
commit
4c4b00a0e9
15 files changed
+263
-21
No files matched your search
@@ -37,6 +37,7 @@ RUN set -euo pipefail; \
|
||||
/bin/bash "${INFINITO_SRC_DIR}/${INFINITO_FILESYSTEM_INSTALL_SCRIPT:?}"
|
||||
|
||||
COPY roles/dev-python/files/shell ${INFINITO_SRC_DIR}/roles/dev-python/files/shell
|
||||
COPY roles/update/files/shell/centos_mirror.sh ${INFINITO_SRC_DIR}/roles/update/files/shell/centos_mirror.sh
|
||||
COPY requirements ${INFINITO_SRC_DIR}/requirements
|
||||
COPY utils/__init__.py ${INFINITO_SRC_DIR}/utils/__init__.py
|
||||
COPY utils/install/__init__.py ${INFINITO_SRC_DIR}/utils/install/__init__.py
|
||||
@@ -44,6 +45,7 @@ COPY utils/install/collections.py ${INFINITO_SRC_DIR}/utils/install/collections.
|
||||
|
||||
# hadolint ignore=DL3008,DL3033,DL3041,SC1090,SC3040
|
||||
RUN set -euo pipefail; \
|
||||
/bin/bash "${INFINITO_SRC_DIR}/roles/update/files/shell/centos_mirror.sh"; \
|
||||
source <(grep -hE '^INFINITO_PYTHON_INSTALL_SCRIPT=' "${INFINITO_SRC_DIR}/default.env"); \
|
||||
/bin/bash "${INFINITO_SRC_DIR}/${INFINITO_PYTHON_INSTALL_SCRIPT:?}" ensure; \
|
||||
VENV="${INFINITO_VENV_DIR}" bash "${INFINITO_SRC_DIR}/scripts/install/venv.sh"; \
|
||||
|
||||
+20
-4
@@ -13,18 +13,34 @@ installed package on the host.
|
||||
|
||||
This role refreshes the package index and upgrades every installed package on
|
||||
the host. `tasks/main.yml` reads `ansible_facts['distribution']` and dispatches
|
||||
to one of three task files grouped by package-manager family. Debian and
|
||||
Ubuntu both use `apt`, Fedora and CentOS both use `dnf`, so each pair shares a
|
||||
single task file. A separate `unsupported.yml` fails loudly with a clear
|
||||
to one task file per package-manager family. Debian and Ubuntu both use
|
||||
`apt`, so they share a single task file; Fedora and CentOS both use `dnf`, and
|
||||
CentOS reaches `fedora.yml` through `centos.yml`. A separate `unsupported.yml` fails loudly with a clear
|
||||
message when a host runs a distribution that this role does not yet cover.
|
||||
|
||||
| `ansible_facts['distribution']` | Task file | Module |
|
||||
| ------------------------------- | ----------------- | -------------------------- |
|
||||
| `Archlinux` | `arch.yml` | `community.general.pacman` |
|
||||
| `Debian` / `Ubuntu` | `debian.yml` | `ansible.builtin.apt` |
|
||||
| `Fedora` / `CentOS` | `fedora.yml` | `ansible.builtin.dnf` |
|
||||
| `Fedora` | `fedora.yml` | `ansible.builtin.dnf` |
|
||||
| `CentOS` | `centos.yml` | `ansible.builtin.dnf` |
|
||||
| anything else | `unsupported.yml` | `ansible.builtin.fail` |
|
||||
|
||||
`centos.yml` first runs [centos_mirror.sh](files/shell/centos_mirror.sh), which replaces every `metalink=` of the CentOS Stream repo files with a `baseurl=` on `mirror.stream.centos.org`, and then includes `fedora.yml`. The [Dockerfile](../../Dockerfile) runs the same script before its first `dnf` call, so the CentOS dev and swarm-node images resolve against the same source.
|
||||
|
||||
## Cosmos
|
||||
|
||||
The diagram places Package update in the Infinito.Nexus cosmos: the components it deploys (capabilities), the central services it consumes (dependencies), and its outward reach (federation and bridged external networks).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph role [update 💻]
|
||||
svc_update["update"]
|
||||
end
|
||||
```
|
||||
|
||||
Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (enabled only in matching deployments); red `0..0` edges are turned off in this role. Node markers show the role's deploy modes (💻 host, 🐳 compose, 🐝 swarm); ❌ marks a service that is explicitly turned off, and ⚙️ an Ansible role dependency declared in `meta/main.yml`.
|
||||
|
||||
## Features
|
||||
|
||||
- **Per-family dispatch:** Routes by `ansible_facts['distribution']` so the
|
||||
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Param: none. Rewrites the CentOS Stream repo files in /etc/yum.repos.d in place and prints the metalink repo of every section it pinned; prints nothing when there is nothing left to pin.
|
||||
set -euo pipefail
|
||||
|
||||
master="https://mirror.stream.centos.org"
|
||||
repo_dir="/etc/yum.repos.d"
|
||||
|
||||
[[ -f "${repo_dir}/centos.repo" ]] || exit 0
|
||||
|
||||
pin() {
|
||||
local metalink_repo="$1" path="$2" file
|
||||
for file in "${repo_dir}/centos.repo" "${repo_dir}/centos-addons.repo"; do
|
||||
[[ -f "${file}" ]] || continue
|
||||
grep -q -E "^metalink=https://mirrors\.centos\.org/metalink\?repo=${metalink_repo}&" "${file}" || continue
|
||||
sed -i -E "s|^metalink=https://mirrors\.centos\.org/metalink\?repo=${metalink_repo}&.*\$|baseurl=${master}/${path}/|" "${file}"
|
||||
echo "${metalink_repo/\[\$\]stream/\$stream}"
|
||||
done
|
||||
}
|
||||
|
||||
declare -A directories=(
|
||||
[baseos]=BaseOS
|
||||
[appstream]=AppStream
|
||||
[crb]=CRB
|
||||
[highavailability]=HighAvailability
|
||||
[nfv]=NFV
|
||||
[rt]=RT
|
||||
[resilientstorage]=ResilientStorage
|
||||
)
|
||||
|
||||
for name in "${!directories[@]}"; do
|
||||
directory="${directories[${name}]}"
|
||||
pin "centos-${name}-[\$]stream" "\$stream/${directory}/\$basearch/os"
|
||||
pin "centos-${name}-debug-[\$]stream" "\$stream/${directory}/\$basearch/debug/tree"
|
||||
pin "centos-${name}-source-[\$]stream" "\$stream/${directory}/source/tree"
|
||||
done
|
||||
|
||||
pin "centos-extras-sig-extras-common-[\$]stream" "SIGs/\$stream/extras/\$basearch/extras-common"
|
||||
pin "centos-extras-sig-extras-common-source-[\$]stream" "SIGs/\$stream/extras/source/extras-common"
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: "📌 update (dnf) | pin the CentOS Stream repos to mirror.stream.centos.org"
|
||||
become: true
|
||||
ansible.builtin.script: shell/centos_mirror.sh
|
||||
register: update_centos_pinned
|
||||
changed_when: update_centos_pinned.stdout | length > 0
|
||||
|
||||
- include_tasks: fedora.yml
|
||||
@@ -7,8 +7,34 @@
|
||||
line: "retries=10"
|
||||
insertafter: "^\\[main\\]"
|
||||
|
||||
- name: "🔄 update (dnf) | refresh dnf cache and upgrade all installed packages"
|
||||
ansible.builtin.dnf: # nocheck: dynamic-package - full-system upgrade, not a package install
|
||||
name: "*"
|
||||
state: latest
|
||||
update_cache: true
|
||||
# Exception: under dnf5, ansible.builtin.dnf's update_cache expires no repo (it queries the repos before loading them), so cached metadata survives until metadata_expire.
|
||||
- name: "🔄 update (dnf) | re-fetch the metadata of every enabled repository"
|
||||
become: true
|
||||
ansible.builtin.command:
|
||||
argv: [dnf, makecache, --refresh]
|
||||
changed_when: false
|
||||
|
||||
- name: "🔄 update (dnf) | upgrade all installed packages"
|
||||
block:
|
||||
- name: "🔄 update (dnf) | refresh dnf cache and upgrade all installed packages"
|
||||
ansible.builtin.dnf: # nocheck: dynamic-package - full-system upgrade, not a package install
|
||||
name: "*"
|
||||
state: latest
|
||||
update_cache: true
|
||||
rescue:
|
||||
- name: "🛑 update (dnf) | re-raise anything but packages no mirror serves" # nocheck: rescue-recovery re-fetches metadata a mirror on the previous push served and retries once; the retry fails loud
|
||||
ansible.builtin.fail:
|
||||
msg: "{{ ansible_failed_result.msg }}"
|
||||
when: ansible_failed_result.msg is not match('Failed to download packages')
|
||||
|
||||
- name: "🧹 update (dnf) | drop the metadata a mirror still on the previous push served"
|
||||
become: true
|
||||
ansible.builtin.command:
|
||||
argv: [dnf, clean, metadata]
|
||||
changed_when: true
|
||||
|
||||
- name: "🔄 update (dnf) | upgrade once more against fresh metadata"
|
||||
ansible.builtin.dnf: # nocheck: dynamic-package - full-system upgrade, not a package install
|
||||
name: "*"
|
||||
state: latest
|
||||
update_cache: true
|
||||
@@ -10,7 +10,7 @@
|
||||
'Debian': 'debian.yml',
|
||||
'Ubuntu': 'debian.yml',
|
||||
'Fedora': 'fedora.yml',
|
||||
'CentOS': 'fedora.yml',
|
||||
'CentOS': 'centos.yml',
|
||||
}[ansible_facts['distribution']] | default('unsupported.yml')
|
||||
}}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ etherpad:
|
||||
# nocheck: dynamic-flag: fixed upstream-compose sub-service, not group-gated
|
||||
bbb-pads:
|
||||
image: alangecker/bbb-docker-pads
|
||||
version: "v1.5.11"
|
||||
version: "v1.5.8"
|
||||
update:
|
||||
type: http_regex
|
||||
url: https://raw.githubusercontent.com/bigbluebutton/docker/develop/repos/tags
|
||||
|
||||
@@ -13,6 +13,7 @@ location = {{ endpoint.path }} {
|
||||
set $up_host "{{ endpoint.upstream | regex_replace('^https?://', '') | regex_replace('/.*$', '') }}";
|
||||
set $bluesky_upstream "{{ endpoint.upstream }}";
|
||||
proxy_pass $bluesky_upstream;
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/upstream_strips.conf.j2' %}
|
||||
proxy_set_header Host $up_host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
@@ -22,3 +23,20 @@ location = {{ endpoint.path }} {
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2' %}
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
location = /ipcc {
|
||||
resolver {{ NETWORK_PUBLIC_DNS_RESOLVERS | join(' ') }} ipv6={{ NETWORK_IPV6_ENABLED | on_off }} valid=300s;
|
||||
resolver_timeout {{ BLUESKY_FRONT_PROXY_TIMEOUT }};
|
||||
proxy_connect_timeout {{ BLUESKY_FRONT_PROXY_TIMEOUT }};
|
||||
proxy_read_timeout {{ BLUESKY_FRONT_PROXY_TIMEOUT }};
|
||||
set $up_host "bsky.app";
|
||||
set $bluesky_ipcc_upstream "https://bsky.app/ipcc";
|
||||
proxy_pass $bluesky_ipcc_upstream;
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/upstream_strips.conf.j2' %}
|
||||
proxy_set_header Host $up_host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_ssl_server_name on;
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/access_control_allow.conf.j2' %}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{% set opentalk_controller_backend = lookup('proxy_pass', application_id, 'controller', 'http', tail='plain', local_port=OPENTALK_CTRL_HTTP_PORT, internal_port=OPENTALK_CTRL_INTERNAL_PORT) %}
|
||||
location /v1/ {
|
||||
{{ opentalk_controller_backend }}
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/upstream_strips.conf.j2' %}
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -13,6 +14,7 @@ location /v1/ {
|
||||
|
||||
location /v1/signaling/ {
|
||||
{{ opentalk_controller_backend }}
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/upstream_strips.conf.j2' %}
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
@@ -26,6 +28,7 @@ location /v1/signaling/ {
|
||||
|
||||
location /livekit {
|
||||
proxy_pass http://127.0.0.1:{{ OPENTALK_LIVEKIT_HTTP_PORT }};
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/upstream_strips.conf.j2' %}
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
internal;
|
||||
{{ prometheus_backend }}
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/forwarded.conf.j2' %}
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/upstream_strips.conf.j2' %}
|
||||
proxy_connect_timeout 2s;
|
||||
proxy_read_timeout 3s;
|
||||
proxy_intercept_errors off;
|
||||
@@ -63,6 +64,7 @@
|
||||
access_log off;
|
||||
{{ prometheus_ready_backend }}
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/forwarded.conf.j2' %}
|
||||
{% include 'roles/sys-svc-proxy/templates/headers/upstream_strips.conf.j2' %}
|
||||
proxy_connect_timeout 3s;
|
||||
proxy_read_timeout 5s;
|
||||
proxy_intercept_errors off;
|
||||
|
||||
@@ -135,13 +135,13 @@ install_pacman() {
|
||||
install_dnf_native() {
|
||||
case "${ID:-}" in
|
||||
fedora)
|
||||
retry_dnf dnf install -y e2fsprogs btrfs-progs
|
||||
retry_dnf dnf --refresh install -y e2fsprogs btrfs-progs
|
||||
return
|
||||
;;
|
||||
esac
|
||||
log "adding EPEL, the only place the EL family carries btrfs-progs"
|
||||
retry_dnf dnf install -y epel-release
|
||||
retry_dnf dnf install -y e2fsprogs btrfs-progs
|
||||
retry_dnf dnf --refresh install -y epel-release
|
||||
retry_dnf dnf --refresh install -y e2fsprogs btrfs-progs
|
||||
}
|
||||
|
||||
# Param: $1 path segment of the zfsonlinux repository, `fedora` or `epel`
|
||||
@@ -233,7 +233,7 @@ install_dnf() {
|
||||
log "zfs userland version: ${zfs_version}"
|
||||
|
||||
if ! command -v rpmbuild >/dev/null 2>&1; then
|
||||
retry_dnf dnf install -y rpm-build
|
||||
retry_dnf dnf --refresh install -y rpm-build
|
||||
fi
|
||||
|
||||
local topdir=/tmp/zfs-kmod-container
|
||||
@@ -261,7 +261,7 @@ SPEC
|
||||
if [ -n "${source_rpm}" ]; then
|
||||
install_zfs_from_source "${source_rpm}" "${before}"
|
||||
else
|
||||
retry_dnf dnf install -y zfs
|
||||
retry_dnf dnf --refresh install -y zfs
|
||||
dnf remove -y zfs-release
|
||||
fi
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ source "${SCRIPT_DIR}/../utils/cache.sh"
|
||||
echo "Snapshotting cache counters before the deploy."
|
||||
CACHE_BEFORE="$(cache_snapshot)"
|
||||
|
||||
echo "Deploying dashboard with all variable (group-conditional) services disabled to speed up the deploy and verify disable= suppresses them from the inventory."
|
||||
make compose-deploy mode=reinstall apps="${DASHBOARD_APP}" disable="$(variable_services dashboard javascript)"
|
||||
echo "Deploying dashboard with all variable (group-conditional) services disabled, except the asset provider, to speed up the deploy and verify disable= suppresses them from the inventory."
|
||||
# Exception: without the asset provider the dashboard loads its logo from the public file.infinito.nexus, which ties this test to that host's uptime; variant 1 pins the provider off, so only round 0 runs here.
|
||||
make compose-deploy mode=reinstall apps="${DASHBOARD_APP}" variant=0 disable="$(variable_services dashboard javascript asset)"
|
||||
inspect
|
||||
|
||||
echo "Actively probing both caches to confirm pull-through works end-to-end."
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Lint: every proxying location the front proxy includes strips upstream headers.
|
||||
|
||||
``proxy_hide_header`` is replaced, not merged, by a location that sets its own,
|
||||
so ``upstream_strips.conf.j2`` only takes effect where a location includes it.
|
||||
A location fragment without it leaks ``X-Powered-By`` and the upstream's
|
||||
``Strict-Transport-Security`` on that path alone.
|
||||
|
||||
Scope: role templates without a ``server`` block. Those are the fragments the
|
||||
front proxy renders into a vhost; a template with its own ``server`` block is
|
||||
the config of an app-internal nginx that sits behind such a location.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from utils.cache.files import iter_project_files, read_text
|
||||
|
||||
from . import PROJECT_ROOT
|
||||
|
||||
_STRIPS = "roles/sys-svc-proxy/templates/headers/upstream_strips.conf.j2"
|
||||
_SERVER_BLOCK = re.compile(r"^\s*server\s*\{", re.MULTILINE)
|
||||
_LOCATION_OPEN = re.compile(r"^\s*location\b[^{]*\{\s*$")
|
||||
_PROXYING = re.compile(r"\bproxy_pass\b|lookup\('proxy_pass'|_backend\s*\}\}")
|
||||
|
||||
|
||||
def _role_templates() -> list[str]:
|
||||
return sorted(
|
||||
path
|
||||
for path in iter_project_files(extensions=(".j2",), exclude_tests=True)
|
||||
if "/roles/" in path and "/templates/" in path
|
||||
)
|
||||
|
||||
|
||||
def _location_blocks(lines: list[str]) -> list[tuple[int, str]]:
|
||||
blocks: list[tuple[int, str]] = []
|
||||
index = 0
|
||||
while index < len(lines):
|
||||
if not _LOCATION_OPEN.match(lines[index]):
|
||||
index += 1
|
||||
continue
|
||||
start = index
|
||||
depth = lines[index].count("{") - lines[index].count("}")
|
||||
body: list[str] = []
|
||||
index += 1
|
||||
while index < len(lines) and depth > 0:
|
||||
depth += lines[index].count("{") - lines[index].count("}")
|
||||
body.append(lines[index])
|
||||
index += 1
|
||||
blocks.append((start + 1, "\n".join(body)))
|
||||
return blocks
|
||||
|
||||
|
||||
class TestProxyLocationStrips(unittest.TestCase):
|
||||
def test_at_least_one_fragment_is_checked(self) -> None:
|
||||
"""Otherwise the assertion below passes over an empty list."""
|
||||
checked = [
|
||||
path
|
||||
for path in _role_templates()
|
||||
if not _SERVER_BLOCK.search(read_text(path))
|
||||
and any(
|
||||
_PROXYING.search(body)
|
||||
for _, body in _location_blocks(read_text(path).splitlines())
|
||||
)
|
||||
]
|
||||
self.assertTrue(checked, "no proxying location fragment found to check")
|
||||
|
||||
def test_proxying_locations_include_the_strips(self) -> None:
|
||||
findings: list[str] = []
|
||||
for path in _role_templates():
|
||||
text = read_text(path)
|
||||
if _SERVER_BLOCK.search(text):
|
||||
continue
|
||||
rel = Path(path).relative_to(PROJECT_ROOT).as_posix()
|
||||
for line, body in _location_blocks(text.splitlines()):
|
||||
if _PROXYING.search(body) and _STRIPS not in body:
|
||||
findings.append(f"- {rel}:{line}")
|
||||
if findings:
|
||||
self.fail(
|
||||
"These proxying locations do not include the upstream header strips, "
|
||||
"so X-Powered-By and the upstream HSTS leak on their path:\n"
|
||||
+ "\n".join(findings)
|
||||
+ f"\n\nFix: add `{{% include '{_STRIPS}' %}}` inside each location."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -179,7 +179,10 @@ class TestOutdated(unittest.TestCase):
|
||||
class TestTemplate(unittest.TestCase):
|
||||
def _outdated(self, services: str, upstream: str) -> list[str]:
|
||||
root = _repo(services)
|
||||
with mock.patch.object(module, "documents", return_value=upstream):
|
||||
with (
|
||||
mock.patch.object(module, "documents", return_value=upstream),
|
||||
mock.patch.object(module, "manifest_exists", return_value=True),
|
||||
):
|
||||
updates = module.outdated(module.collect_entries(root), root)
|
||||
return [update.latest for update in updates]
|
||||
|
||||
@@ -206,6 +209,31 @@ class TestTemplate(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestPulledImage(unittest.TestCase):
|
||||
def _outdated(self, exists: bool | None) -> tuple[list[str], mock.MagicMock]:
|
||||
root = _repo(COMPOUND)
|
||||
with (
|
||||
mock.patch.object(
|
||||
module, "documents", return_value="core v3.0.39\npart v1.11.3\n"
|
||||
),
|
||||
mock.patch.object(module, "manifest_exists", return_value=exists) as probe,
|
||||
):
|
||||
updates = module.outdated(module.collect_entries(root), root)
|
||||
return [update.latest for update in updates], probe
|
||||
|
||||
def test_a_tag_the_image_registry_lacks_is_not_pinned(self) -> None:
|
||||
latest, probe = self._outdated(False)
|
||||
self.assertEqual(latest, [])
|
||||
probe.assert_called_once_with("example/part", "v1.11.3-v3.0.39")
|
||||
|
||||
def test_an_indeterminate_registry_answer_keeps_the_bump(self) -> None:
|
||||
self.assertEqual(self._outdated(None)[0], ["v1.11.3-v3.0.39"])
|
||||
|
||||
def test_only_the_version_of_a_pulled_image_carries_the_image(self) -> None:
|
||||
entries = {e.key: e.image for e in module.collect_entries(_repo())}
|
||||
self.assertEqual(entries, {"app_version": "", "version": "example"})
|
||||
|
||||
|
||||
class TestAddons(unittest.TestCase):
|
||||
def test_only_a_declared_or_github_released_addon_pin_is_collected(self) -> None:
|
||||
entries = module.collect_entries(_addon_repo())
|
||||
|
||||
+12
-2
@@ -40,6 +40,7 @@ from urllib.parse import urlencode
|
||||
from utils.annotations.suppress import is_suppressed_at
|
||||
from utils.cache.files import read_text
|
||||
from utils.cache.yaml import load_yaml
|
||||
from utils.docker.registry import manifest_exists
|
||||
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
||||
from utils.update import checksum
|
||||
from utils.update.addons import GITHUB_RELEASES_CATALOG, iter_addon_files
|
||||
@@ -86,6 +87,10 @@ class VersionSourceEntry:
|
||||
config_path: file that carries the pin.
|
||||
line: 1-indexed line of the pin.
|
||||
addon: whether the pin sits in ``meta/addons/<id>.yml``.
|
||||
image: image the pin tags when it is the entity's ``version``, pulled
|
||||
directly or as the base of a ``custom`` build; a bump to a tag
|
||||
its registry answers 404 for is withheld, an indeterminate answer
|
||||
does not block it.
|
||||
"""
|
||||
|
||||
role: str
|
||||
@@ -96,6 +101,7 @@ class VersionSourceEntry:
|
||||
config_path: Path
|
||||
line: int
|
||||
addon: bool = False
|
||||
image: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -416,6 +422,7 @@ def collect_entries(repo_root: Path) -> list[VersionSourceEntry]:
|
||||
source=source,
|
||||
config_path=config_path,
|
||||
line=line,
|
||||
image=config.get("image", "") if key == DEFAULT_KEY else "",
|
||||
)
|
||||
)
|
||||
return entries + _addon_entries(repo_root)
|
||||
@@ -432,8 +439,11 @@ def outdated(
|
||||
candidates(entry, repo_root),
|
||||
assembled=bool(entry.source.get("template")),
|
||||
)
|
||||
if newest:
|
||||
updates.append(VersionSourceUpdate(entry=entry, latest=newest))
|
||||
if not newest:
|
||||
continue
|
||||
if entry.image and manifest_exists(entry.image, newest) is False:
|
||||
continue
|
||||
updates.append(VersionSourceUpdate(entry=entry, latest=newest))
|
||||
return updates
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user