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.
@@ -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 DockerConfig, DockerPackageUpdateInfo, NodeConfig, PackageUpdateInfo
20
- from updates2mqtt.model import Discovery, ReleaseProvider
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.version_include: str | None = None
55
- self.version_exclude: str | None = None
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
- self.node_cfg: NodeConfig = node_cfg
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.api_throttle_wait
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, original_discovery=discovery)
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, original_discovery: Discovery | None = None) -> Discovery | None:
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
- latest_version: str | None = NO_KNOWN_IMAGE
304
- registry_throttled = self.check_throttle(repo_id)
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
- log.debug(
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
- latest_version = reg_data.short_id[7:] if reg_data else None
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
- logger.warn("Docker Registry throttling requests, %s", e.explanation)
322
- self.pause_api_until[repo_id] = time.time() + self.api_throttle_pause
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
- local_version: str | None = NO_KNOWN_IMAGE
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
- local_version = latest_version if latest_version in local_versions else local_versions[0]
334
- log.debug(f"Setting local version to {local_version}, local_versions:{local_versions}")
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
- if registry_throttled:
347
- custom["registry_throttled"] = True
348
- save_if_set("compose_path", c.labels.get("com.docker.compose.project.working_dir"))
349
- save_if_set("compose_version", c.labels.get("com.docker.compose.version"))
350
- save_if_set("compose_service", c.labels.get("com.docker.compose.service"))
351
- save_if_set("git_repo_path", customization.git_repo_path)
352
- # save_if_set("apt_pkgs", c_env.get("UPD2MQTT_APT_PKGS"))
353
-
354
- if customization.update == "AUTO":
355
- logger.debug("Auto update policy detected")
356
- update_policy = "Auto"
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
- update_policy = "Passive"
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 (local_version != NO_KNOWN_IMAGE or latest_version != NO_KNOWN_IMAGE)
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 not available, image_ref:{image_ref},local_version:{local_version},latest_version:{latest_version}"
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
- log.debug(
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 local_version is None or local_version == NO_KNOWN_IMAGE:
402
- local_version = git_local_version(full_repo_path, Path(self.node_cfg.git_path)) or NO_KNOWN_IMAGE
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 local_version is not None and local_version.startswith("git:"):
407
- latest_version = f"{local_version}+{behind_count}"
408
- log.info("Git update available, generating version %s", latest_version)
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 skip_pull:
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
- current_version=local_version,
515
+ release_summary=release_summary,
516
+ current_version=public_installed_version,
517
+ publish_policy=publish_policy,
446
518
  update_policy=update_policy,
447
- update_last_attempt=original_discovery.update_last_attempt if original_discovery else None,
448
- latest_version=latest_version if latest_version != NO_KNOWN_IMAGE else local_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
- def match(pkg: PackageUpdateInfo) -> bool:
539
- if pkg is not None and pkg.docker is not None and pkg.docker.image_name is not None:
540
- if image_name is not None and image_name == pkg.docker.image_name:
541
- return True
542
- if image_ref is not None and image_ref == pkg.docker.image_name:
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")