updates2mqtt 1.6.0__py3-none-any.whl → 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- updates2mqtt/app.py +10 -7
- updates2mqtt/config.py +30 -19
- updates2mqtt/hass_formatter.py +7 -17
- updates2mqtt/integrations/docker.py +148 -149
- updates2mqtt/integrations/docker_enrich.py +344 -0
- updates2mqtt/model.py +134 -16
- updates2mqtt/mqtt.py +26 -5
- {updates2mqtt-1.6.0.dist-info → updates2mqtt-1.7.0.dist-info}/METADATA +9 -13
- updates2mqtt-1.7.0.dist-info/RECORD +16 -0
- {updates2mqtt-1.6.0.dist-info → updates2mqtt-1.7.0.dist-info}/WHEEL +2 -2
- updates2mqtt-1.6.0.dist-info/RECORD +0 -15
- {updates2mqtt-1.6.0.dist-info → updates2mqtt-1.7.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import re
|
|
2
1
|
import subprocess
|
|
3
2
|
import time
|
|
4
3
|
import typing
|
|
@@ -14,10 +13,25 @@ import docker.errors
|
|
|
14
13
|
import structlog
|
|
15
14
|
from docker.auth import resolve_repository_name
|
|
16
15
|
from docker.models.containers import Container
|
|
17
|
-
from hishel.httpx import SyncCacheClient
|
|
18
16
|
|
|
19
|
-
from updates2mqtt.config import
|
|
20
|
-
|
|
17
|
+
from updates2mqtt.config import (
|
|
18
|
+
NO_KNOWN_IMAGE,
|
|
19
|
+
DockerConfig,
|
|
20
|
+
NodeConfig,
|
|
21
|
+
PackageUpdateInfo,
|
|
22
|
+
PublishPolicy,
|
|
23
|
+
UpdatePolicy,
|
|
24
|
+
)
|
|
25
|
+
from updates2mqtt.integrations.docker_enrich import (
|
|
26
|
+
AuthError,
|
|
27
|
+
CommonPackageEnricher,
|
|
28
|
+
DefaultPackageEnricher,
|
|
29
|
+
LabelEnricher,
|
|
30
|
+
LinuxServerIOPackageEnricher,
|
|
31
|
+
PackageEnricher,
|
|
32
|
+
SourceReleaseEnricher,
|
|
33
|
+
)
|
|
34
|
+
from updates2mqtt.model import Discovery, ReleaseProvider, Selection, VersionPolicy, select_version
|
|
21
35
|
|
|
22
36
|
from .git_utils import git_check_update_available, git_iso_timestamp, git_local_version, git_pull, git_trust
|
|
23
37
|
|
|
@@ -27,7 +41,6 @@ if typing.TYPE_CHECKING:
|
|
|
27
41
|
# distinguish docker build from docker pull?
|
|
28
42
|
|
|
29
43
|
log = structlog.get_logger()
|
|
30
|
-
NO_KNOWN_IMAGE = "UNKNOWN"
|
|
31
44
|
|
|
32
45
|
|
|
33
46
|
class DockerComposeCommand(Enum):
|
|
@@ -51,8 +64,8 @@ class ContainerCustomization:
|
|
|
51
64
|
self.picture: str | None = None
|
|
52
65
|
self.relnotes: str | None = None
|
|
53
66
|
self.ignore: bool = False
|
|
54
|
-
self.
|
|
55
|
-
self.
|
|
67
|
+
self.version_policy: VersionPolicy | None = None
|
|
68
|
+
self.registry_token: str | None = None
|
|
56
69
|
|
|
57
70
|
if not container.attrs or container.attrs.get("Config") is None:
|
|
58
71
|
return
|
|
@@ -93,6 +106,8 @@ class ContainerCustomization:
|
|
|
93
106
|
if v is not None:
|
|
94
107
|
if isinstance(getattr(self, attr), bool):
|
|
95
108
|
setattr(self, attr, v.upper() in ("TRUE", "YES", "1"))
|
|
109
|
+
elif isinstance(getattr(self, attr), VersionPolicy):
|
|
110
|
+
setattr(self, attr, VersionPolicy[v.upper()])
|
|
96
111
|
else:
|
|
97
112
|
setattr(self, attr, v)
|
|
98
113
|
|
|
@@ -103,20 +118,28 @@ class DockerProvider(ReleaseProvider):
|
|
|
103
118
|
def __init__(
|
|
104
119
|
self,
|
|
105
120
|
cfg: DockerConfig,
|
|
106
|
-
common_pkg_cfg: dict[str, PackageUpdateInfo],
|
|
107
121
|
node_cfg: NodeConfig,
|
|
108
122
|
self_bounce: Event | None = None,
|
|
109
123
|
) -> None:
|
|
110
|
-
super().__init__("docker")
|
|
124
|
+
super().__init__(node_cfg, "docker")
|
|
111
125
|
self.client: docker.DockerClient = docker.from_env()
|
|
112
126
|
self.cfg: DockerConfig = cfg
|
|
113
|
-
|
|
114
|
-
self.common_pkgs: dict[str, PackageUpdateInfo] = common_pkg_cfg if common_pkg_cfg else {}
|
|
127
|
+
|
|
115
128
|
# TODO: refresh discovered packages periodically
|
|
116
|
-
self.discovered_pkgs: dict[str, PackageUpdateInfo] = self.discover_metadata()
|
|
117
129
|
self.pause_api_until: dict[str, float] = {}
|
|
118
|
-
self.api_throttle_pause: int = cfg.
|
|
130
|
+
self.api_throttle_pause: int = cfg.default_api_backoff
|
|
119
131
|
self.self_bounce: Event | None = self_bounce
|
|
132
|
+
self.pkg_enrichers: list[PackageEnricher] = [
|
|
133
|
+
CommonPackageEnricher(self.cfg),
|
|
134
|
+
LinuxServerIOPackageEnricher(self.cfg),
|
|
135
|
+
DefaultPackageEnricher(self.cfg),
|
|
136
|
+
]
|
|
137
|
+
self.label_enricher = LabelEnricher()
|
|
138
|
+
self.release_enricher = SourceReleaseEnricher()
|
|
139
|
+
|
|
140
|
+
def initialize(self) -> None:
|
|
141
|
+
for enricher in self.pkg_enrichers:
|
|
142
|
+
enricher.initialize()
|
|
120
143
|
|
|
121
144
|
def update(self, discovery: Discovery) -> bool:
|
|
122
145
|
logger: Any = self.log.bind(container=discovery.name, action="update")
|
|
@@ -221,7 +244,7 @@ class DockerProvider(ReleaseProvider):
|
|
|
221
244
|
try:
|
|
222
245
|
c: Container = self.client.containers.get(discovery.name)
|
|
223
246
|
if c:
|
|
224
|
-
rediscovery = self.analyze(c, discovery.session,
|
|
247
|
+
rediscovery = self.analyze(c, discovery.session, previous_discovery=discovery)
|
|
225
248
|
if rediscovery:
|
|
226
249
|
self.discoveries[rediscovery.name] = rediscovery
|
|
227
250
|
return rediscovery
|
|
@@ -236,13 +259,13 @@ class DockerProvider(ReleaseProvider):
|
|
|
236
259
|
if self.pause_api_until.get(repo_id) is not None:
|
|
237
260
|
if self.pause_api_until[repo_id] < time.time():
|
|
238
261
|
del self.pause_api_until[repo_id]
|
|
239
|
-
log.info("%s throttling wait complete", repo_id)
|
|
262
|
+
self.log.info("%s throttling wait complete", repo_id)
|
|
240
263
|
else:
|
|
241
|
-
log.debug("%s throttling has %s secs left", repo_id, self.pause_api_until[repo_id] - time.time())
|
|
264
|
+
self.log.debug("%s throttling has %s secs left", repo_id, self.pause_api_until[repo_id] - time.time())
|
|
242
265
|
return True
|
|
243
266
|
return False
|
|
244
267
|
|
|
245
|
-
def analyze(self, c: Container, session: str,
|
|
268
|
+
def analyze(self, c: Container, session: str, previous_discovery: Discovery | None = None) -> Discovery | None:
|
|
246
269
|
logger = self.log.bind(container=c.name, action="analyze")
|
|
247
270
|
|
|
248
271
|
image_ref: str | None = None
|
|
@@ -281,12 +304,24 @@ class DockerProvider(ReleaseProvider):
|
|
|
281
304
|
logger.warn("Cannot determine local version: %s", e)
|
|
282
305
|
logger.warn("RepoDigests=%s", image.attrs.get("RepoDigests"))
|
|
283
306
|
|
|
307
|
+
selection = Selection(self.cfg.image_ref_select, image_ref)
|
|
308
|
+
publish_policy: PublishPolicy = PublishPolicy.MQTT if not selection.result else PublishPolicy.HOMEASSISTANT
|
|
309
|
+
version_policy: VersionPolicy = VersionPolicy.AUTO if not customization.version_policy else customization.version_policy
|
|
310
|
+
|
|
311
|
+
if customization.update == "AUTO":
|
|
312
|
+
logger.debug("Auto update policy detected")
|
|
313
|
+
update_policy: UpdatePolicy = UpdatePolicy.AUTO
|
|
314
|
+
else:
|
|
315
|
+
update_policy = UpdatePolicy.PASSIVE
|
|
316
|
+
|
|
284
317
|
platform: str = "Unknown"
|
|
285
318
|
pkg_info: PackageUpdateInfo = self.default_metadata(image_name, image_ref=image_ref)
|
|
286
319
|
|
|
287
320
|
try:
|
|
288
|
-
picture_url = customization.picture or pkg_info.logo_url
|
|
289
|
-
relnotes_url = customization.relnotes or pkg_info.release_notes_url
|
|
321
|
+
picture_url: str | None = customization.picture or pkg_info.logo_url
|
|
322
|
+
relnotes_url: str | None = customization.relnotes or pkg_info.release_notes_url
|
|
323
|
+
release_summary: str | None = None
|
|
324
|
+
|
|
290
325
|
if image is not None and image.attrs is not None:
|
|
291
326
|
platform = "/".join(
|
|
292
327
|
filter(
|
|
@@ -300,8 +335,10 @@ class DockerProvider(ReleaseProvider):
|
|
|
300
335
|
)
|
|
301
336
|
|
|
302
337
|
reg_data: RegistryData | None = None
|
|
303
|
-
|
|
304
|
-
|
|
338
|
+
latest_digest: str | None = NO_KNOWN_IMAGE
|
|
339
|
+
latest_version: str | None = None
|
|
340
|
+
|
|
341
|
+
registry_throttled: bool = self.check_throttle(repo_id)
|
|
305
342
|
|
|
306
343
|
if image_ref and local_versions and not registry_throttled:
|
|
307
344
|
retries_left = 3
|
|
@@ -309,17 +346,23 @@ class DockerProvider(ReleaseProvider):
|
|
|
309
346
|
try:
|
|
310
347
|
logger.debug("Fetching registry data", image_ref=image_ref)
|
|
311
348
|
reg_data = self.client.images.get_registry_data(image_ref)
|
|
312
|
-
|
|
349
|
+
logger.debug(
|
|
313
350
|
"Registry Data: id:%s,image:%s, attrs:%s",
|
|
314
351
|
reg_data.id,
|
|
315
352
|
reg_data.image_name,
|
|
316
353
|
reg_data.attrs,
|
|
317
354
|
)
|
|
318
|
-
|
|
355
|
+
latest_digest = reg_data.short_id[7:] if reg_data else None
|
|
356
|
+
|
|
319
357
|
except docker.errors.APIError as e:
|
|
320
358
|
if e.status_code == HTTPStatus.TOO_MANY_REQUESTS:
|
|
321
|
-
|
|
322
|
-
|
|
359
|
+
retry_secs: int
|
|
360
|
+
try:
|
|
361
|
+
retry_secs = int(e.response.headers.get("Retry-After", self.api_throttle_pause)) # type: ignore[union-attr]
|
|
362
|
+
except: # noqa: E722
|
|
363
|
+
retry_secs = self.api_throttle_pause
|
|
364
|
+
logger.warn("Docker Registry throttling requests for %s seconds, %s", retry_secs, e.explanation)
|
|
365
|
+
self.pause_api_until[repo_id] = time.time() + retries_left
|
|
323
366
|
return None
|
|
324
367
|
retries_left -= 1
|
|
325
368
|
if retries_left == 0 or e.is_client_error():
|
|
@@ -327,11 +370,12 @@ class DockerProvider(ReleaseProvider):
|
|
|
327
370
|
else:
|
|
328
371
|
logger.debug("Failed to fetch registry data, retrying: %s", e)
|
|
329
372
|
|
|
330
|
-
|
|
373
|
+
installed_digest: str | None = NO_KNOWN_IMAGE
|
|
374
|
+
installed_version: str | None = None
|
|
331
375
|
if local_versions:
|
|
332
376
|
# might be multiple RepoDigests if image has been pulled multiple times with diff manifests
|
|
333
|
-
|
|
334
|
-
|
|
377
|
+
installed_digest = latest_digest if latest_digest in local_versions else local_versions[0]
|
|
378
|
+
logger.debug(f"Setting local digest to {installed_digest}, local_versions:{local_versions}")
|
|
335
379
|
|
|
336
380
|
def save_if_set(key: str, val: str | None) -> None:
|
|
337
381
|
if val is not None:
|
|
@@ -339,23 +383,55 @@ class DockerProvider(ReleaseProvider):
|
|
|
339
383
|
|
|
340
384
|
image_ref = image_ref or ""
|
|
341
385
|
|
|
342
|
-
custom: dict[str, str | bool] = {}
|
|
386
|
+
custom: dict[str, str | bool | int | list[str] | dict[str, Any] | None] = {}
|
|
343
387
|
custom["platform"] = platform
|
|
344
388
|
custom["image_ref"] = image_ref
|
|
389
|
+
custom["installed_digest"] = installed_digest
|
|
390
|
+
custom["latest_digest"] = latest_digest
|
|
345
391
|
custom["repo_id"] = repo_id
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
392
|
+
custom["git_repo_path"] = customization.git_repo_path
|
|
393
|
+
|
|
394
|
+
if c.labels:
|
|
395
|
+
save_if_set("compose_path", c.labels.get("com.docker.compose.project.working_dir"))
|
|
396
|
+
save_if_set("compose_version", c.labels.get("com.docker.compose.version"))
|
|
397
|
+
save_if_set("compose_service", c.labels.get("com.docker.compose.service"))
|
|
398
|
+
save_if_set("documentation_url", c.labels.get("org.opencontainers.image.documentation"))
|
|
399
|
+
save_if_set("description", c.labels.get("org.opencontainers.image.description"))
|
|
400
|
+
save_if_set("current_image_created", c.labels.get("org.opencontainers.image.created"))
|
|
401
|
+
save_if_set("current_image_version", c.labels.get("org.opencontainers.image.version"))
|
|
402
|
+
save_if_set("vendor", c.labels.get("org.opencontainers.image.vendor"))
|
|
403
|
+
installed_version = c.labels.get("org.opencontainers.image.version")
|
|
357
404
|
else:
|
|
358
|
-
|
|
405
|
+
logger.debug("No annotations found on local container")
|
|
406
|
+
# save_if_set("apt_pkgs", c_env.get("UPD2MQTT_APT_PKGS"))
|
|
407
|
+
|
|
408
|
+
if latest_digest is None or latest_digest == NO_KNOWN_IMAGE or registry_throttled:
|
|
409
|
+
logger.debug(
|
|
410
|
+
"Skipping image manifest enrichment",
|
|
411
|
+
latest_digest=latest_digest,
|
|
412
|
+
image_ref=image_ref,
|
|
413
|
+
platform=platform,
|
|
414
|
+
throttled=registry_throttled,
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
os, arch = platform.split("/")[:2] if "/" in platform else (platform, "Unknown")
|
|
418
|
+
try:
|
|
419
|
+
annotations: dict[str, str] = self.label_enricher.fetch_annotations(
|
|
420
|
+
image_ref, os, arch, token=customization.registry_token
|
|
421
|
+
)
|
|
422
|
+
except AuthError as e:
|
|
423
|
+
logger.warning("Authentication error prevented Docker Registry entichment: %s", e)
|
|
424
|
+
annotations = {}
|
|
425
|
+
|
|
426
|
+
if annotations:
|
|
427
|
+
save_if_set("latest_image_created", annotations.get("org.opencontainers.image.created"))
|
|
428
|
+
save_if_set("source", annotations.get("org.opencontainers.image.source"))
|
|
429
|
+
save_if_set("documentation_url", annotations.get("org.opencontainers.image.documentation"))
|
|
430
|
+
save_if_set("description", annotations.get("org.opencontainers.image.description"))
|
|
431
|
+
save_if_set("latest_image_version", annotations.get("org.opencontainers.image.version"))
|
|
432
|
+
save_if_set("vendor", annotations.get("org.opencontainers.image.vendor"))
|
|
433
|
+
latest_version = annotations.get("org.opencontainers.image.version")
|
|
434
|
+
custom.update(self.release_enricher.enrich(annotations) or {})
|
|
359
435
|
|
|
360
436
|
if custom.get("git_repo_path") and custom.get("compose_path"):
|
|
361
437
|
full_repo_path: Path = Path(cast("str", custom.get("compose_path"))).joinpath(
|
|
@@ -369,43 +445,33 @@ class DockerProvider(ReleaseProvider):
|
|
|
369
445
|
self.cfg.allow_pull
|
|
370
446
|
and image_ref is not None
|
|
371
447
|
and image_ref != ""
|
|
372
|
-
and (
|
|
448
|
+
and (installed_digest != NO_KNOWN_IMAGE or latest_digest != NO_KNOWN_IMAGE)
|
|
373
449
|
)
|
|
374
450
|
if self.cfg.allow_pull and not can_pull:
|
|
375
451
|
logger.debug(
|
|
376
|
-
f"Pull
|
|
452
|
+
f"Pull unavailable, image_ref:{image_ref},installed_digest:{installed_digest},latest_digest:{latest_digest}"
|
|
377
453
|
)
|
|
378
|
-
skip_pull: bool = False
|
|
379
|
-
if can_pull and latest_version is not None:
|
|
380
|
-
if customization.version_include and not re.match(customization.version_include, latest_version):
|
|
381
|
-
logger.info(f"Skipping version {latest_version} not matching include pattern")
|
|
382
|
-
skip_pull = True
|
|
383
|
-
latest_version = local_version
|
|
384
|
-
if customization.version_exclude and re.match(customization.version_exclude, latest_version): # type: ignore[arg-type]
|
|
385
|
-
logger.info(f"Skipping version {latest_version} matching exclude pattern")
|
|
386
|
-
skip_pull = True
|
|
387
|
-
latest_version = local_version
|
|
388
454
|
|
|
389
455
|
can_build: bool = False
|
|
390
456
|
if self.cfg.allow_build:
|
|
391
457
|
can_build = custom.get("git_repo_path") is not None and custom.get("compose_path") is not None
|
|
392
458
|
if not can_build:
|
|
393
459
|
if custom.get("git_repo_path") is not None:
|
|
394
|
-
|
|
460
|
+
logger.debug(
|
|
395
461
|
"Local build ignored for git_repo_path=%s because no compose_path", custom.get("git_repo_path")
|
|
396
462
|
)
|
|
397
463
|
else:
|
|
398
464
|
full_repo_path = self.full_repo_path(
|
|
399
465
|
cast("str", custom.get("compose_path")), cast("str", custom.get("git_repo_path"))
|
|
400
466
|
)
|
|
401
|
-
if
|
|
402
|
-
|
|
467
|
+
if installed_digest is None or installed_digest == NO_KNOWN_IMAGE:
|
|
468
|
+
installed_digest = git_local_version(full_repo_path, Path(self.node_cfg.git_path)) or NO_KNOWN_IMAGE
|
|
403
469
|
|
|
404
470
|
behind_count: int = git_check_update_available(full_repo_path, Path(self.node_cfg.git_path))
|
|
405
471
|
if behind_count > 0:
|
|
406
|
-
if
|
|
407
|
-
|
|
408
|
-
|
|
472
|
+
if installed_digest is not None and installed_digest.startswith("git:"):
|
|
473
|
+
latest_digest = f"{installed_digest}+{behind_count}"
|
|
474
|
+
logger.info("Git update available, generating version %s", latest_digest)
|
|
409
475
|
else:
|
|
410
476
|
logger.debug(f"Git update not available, local repo:{full_repo_path}")
|
|
411
477
|
|
|
@@ -422,19 +488,23 @@ class DockerProvider(ReleaseProvider):
|
|
|
422
488
|
logger.info(f"Update not available, can_pull:{can_pull}, can_build:{can_build},can_restart{can_restart}")
|
|
423
489
|
if relnotes_url:
|
|
424
490
|
features.append("RELEASE_NOTES")
|
|
425
|
-
if
|
|
426
|
-
update_type: str = "Skipped"
|
|
427
|
-
elif can_pull:
|
|
491
|
+
if can_pull:
|
|
428
492
|
update_type = "Docker Image"
|
|
429
493
|
elif can_build:
|
|
430
494
|
update_type = "Docker Build"
|
|
431
495
|
else:
|
|
432
496
|
update_type = "Unavailable"
|
|
433
497
|
custom["can_pull"] = can_pull
|
|
434
|
-
custom["skip_pull"] = skip_pull
|
|
435
498
|
# can_pull,can_build etc are only info flags
|
|
436
499
|
# the HASS update process is driven by comparing current and available versions
|
|
437
500
|
|
|
501
|
+
public_installed_version = select_version(
|
|
502
|
+
version_policy, installed_version, installed_digest, other_version=latest_version, other_digest=latest_digest
|
|
503
|
+
)
|
|
504
|
+
public_latest_version = select_version(
|
|
505
|
+
version_policy, latest_version, latest_digest, other_version=installed_version, other_digest=installed_digest
|
|
506
|
+
)
|
|
507
|
+
|
|
438
508
|
discovery: Discovery = Discovery(
|
|
439
509
|
self,
|
|
440
510
|
c.name,
|
|
@@ -442,10 +512,12 @@ class DockerProvider(ReleaseProvider):
|
|
|
442
512
|
node=self.node_cfg.name,
|
|
443
513
|
entity_picture_url=picture_url,
|
|
444
514
|
release_url=relnotes_url,
|
|
445
|
-
|
|
515
|
+
release_summary=release_summary,
|
|
516
|
+
current_version=public_installed_version,
|
|
517
|
+
publish_policy=publish_policy,
|
|
446
518
|
update_policy=update_policy,
|
|
447
|
-
|
|
448
|
-
latest_version=
|
|
519
|
+
version_policy=version_policy,
|
|
520
|
+
latest_version=public_latest_version,
|
|
449
521
|
device_icon=self.cfg.device_icon,
|
|
450
522
|
can_update=can_update,
|
|
451
523
|
update_type=update_type,
|
|
@@ -455,6 +527,7 @@ class DockerProvider(ReleaseProvider):
|
|
|
455
527
|
custom=custom,
|
|
456
528
|
features=features,
|
|
457
529
|
throttled=registry_throttled,
|
|
530
|
+
previous=previous_discovery,
|
|
458
531
|
)
|
|
459
532
|
logger.debug("Analyze generated discovery: %s", discovery)
|
|
460
533
|
return discovery
|
|
@@ -463,6 +536,10 @@ class DockerProvider(ReleaseProvider):
|
|
|
463
536
|
logger.debug("Analyze returned empty discovery")
|
|
464
537
|
return None
|
|
465
538
|
|
|
539
|
+
# def version(self, c: Container, version_type: str):
|
|
540
|
+
# metadata_version: str = c.labels.get("org.opencontainers.image.version")
|
|
541
|
+
# metadata_revision: str = c.labels.get("org.opencontainers.image.revision")
|
|
542
|
+
|
|
466
543
|
async def scan(self, session: str) -> AsyncGenerator[Discovery]:
|
|
467
544
|
logger = self.log.bind(session=session, action="scan", source=self.source_type)
|
|
468
545
|
containers: int = 0
|
|
@@ -521,87 +598,9 @@ class DockerProvider(ReleaseProvider):
|
|
|
521
598
|
def resolve(self, discovery_name: str) -> Discovery | None:
|
|
522
599
|
return self.discoveries.get(discovery_name)
|
|
523
600
|
|
|
524
|
-
def hass_state_format(self, discovery: Discovery) -> dict: # noqa: ARG002
|
|
525
|
-
# disable since hass mqtt update has strict json schema for message
|
|
526
|
-
return {
|
|
527
|
-
# "docker_image_ref": discovery.custom.get("image_ref"),
|
|
528
|
-
# "last_update_attempt": safe_json_dt(discovery.update_last_attempt),
|
|
529
|
-
# "can_pull": discovery.custom.get("can_pull"),
|
|
530
|
-
# "can_build": discovery.custom.get("can_build"),
|
|
531
|
-
# "can_restart": discovery.custom.get("can_restart"),
|
|
532
|
-
# "git_repo_path": discovery.custom.get("git_repo_path"),
|
|
533
|
-
# "compose_path": discovery.custom.get("compose_path"),
|
|
534
|
-
# "platform": discovery.custom.get("platform"),
|
|
535
|
-
}
|
|
536
|
-
|
|
537
601
|
def default_metadata(self, image_name: str | None, image_ref: str | None) -> PackageUpdateInfo:
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
return True
|
|
544
|
-
return False
|
|
545
|
-
|
|
546
|
-
if image_name is not None and image_ref is not None:
|
|
547
|
-
for pkg in self.common_pkgs.values():
|
|
548
|
-
if match(pkg):
|
|
549
|
-
self.log.debug(
|
|
550
|
-
"Found common package",
|
|
551
|
-
image_name=pkg.docker.image_name, # type: ignore [union-attr]
|
|
552
|
-
logo_url=pkg.logo_url,
|
|
553
|
-
relnotes_url=pkg.release_notes_url,
|
|
554
|
-
)
|
|
555
|
-
return pkg
|
|
556
|
-
for pkg in self.discovered_pkgs.values():
|
|
557
|
-
if match(pkg):
|
|
558
|
-
self.log.debug(
|
|
559
|
-
"Found discovered package",
|
|
560
|
-
pkg=pkg.docker.image_name, # type: ignore [union-attr]
|
|
561
|
-
logo_url=pkg.logo_url,
|
|
562
|
-
relnotes_url=pkg.release_notes_url,
|
|
563
|
-
)
|
|
564
|
-
return pkg
|
|
565
|
-
|
|
566
|
-
self.log.debug("No common or discovered package found", image_name=image_name)
|
|
567
|
-
return PackageUpdateInfo(
|
|
568
|
-
DockerPackageUpdateInfo(image_name or NO_KNOWN_IMAGE),
|
|
569
|
-
logo_url=self.cfg.default_entity_picture_url,
|
|
570
|
-
release_notes_url=None,
|
|
571
|
-
)
|
|
572
|
-
|
|
573
|
-
def discover_metadata(self) -> dict[str, PackageUpdateInfo]:
|
|
574
|
-
pkgs: dict[str, PackageUpdateInfo] = {}
|
|
575
|
-
cfg = self.cfg.discover_metadata.get("linuxserver.io")
|
|
576
|
-
if cfg and cfg.enabled:
|
|
577
|
-
linuxserver_metadata(pkgs, cache_ttl=cfg.cache_ttl)
|
|
578
|
-
return pkgs
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
def linuxserver_metadata_api(cache_ttl: int) -> dict:
|
|
582
|
-
"""Fetch and cache linuxserver.io API call for image metadata"""
|
|
583
|
-
try:
|
|
584
|
-
with SyncCacheClient(headers=[("cache-control", f"max-age={cache_ttl}")]) as client:
|
|
585
|
-
log.debug(f"Fetching linuxserver.io metadata from API, cache_ttl={cache_ttl}")
|
|
586
|
-
req = client.get("https://api.linuxserver.io/api/v1/images?include_config=false&include_deprecated=false")
|
|
587
|
-
return req.json()
|
|
588
|
-
except Exception:
|
|
589
|
-
log.exception("Failed to fetch linuxserver.io metadata")
|
|
590
|
-
return {}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
def linuxserver_metadata(discovered_pkgs: dict[str, PackageUpdateInfo], cache_ttl: int) -> None:
|
|
594
|
-
"""Fetch linuxserver.io metadata for all their images via their API"""
|
|
595
|
-
repos: list = linuxserver_metadata_api(cache_ttl).get("data", {}).get("repositories", {}).get("linuxserver", [])
|
|
596
|
-
added = 0
|
|
597
|
-
for repo in repos:
|
|
598
|
-
image_name = repo.get("name")
|
|
599
|
-
if image_name and image_name not in discovered_pkgs:
|
|
600
|
-
discovered_pkgs[image_name] = PackageUpdateInfo(
|
|
601
|
-
DockerPackageUpdateInfo(f"lscr.io/linuxserver/{image_name}"),
|
|
602
|
-
logo_url=repo["project_logo"],
|
|
603
|
-
release_notes_url=f"{repo['github_url']}/releases",
|
|
604
|
-
)
|
|
605
|
-
added += 1
|
|
606
|
-
log.debug("Added linuxserver.io package", pkg=image_name)
|
|
607
|
-
log.info(f"Added {added} linuxserver.io package details")
|
|
602
|
+
for enricher in self.pkg_enrichers:
|
|
603
|
+
pkg_info = enricher.enrich(image_name, image_ref, self.log)
|
|
604
|
+
if pkg_info is not None:
|
|
605
|
+
return pkg_info
|
|
606
|
+
raise ValueError("No enricher could provide metadata, not even default enricher")
|