From 86665ba542137d5d1bdd0d5016fd4ce3e05acdf8 Mon Sep 17 00:00:00 2001 From: Philip Dakowitz Date: Tue, 27 Jan 2026 17:49:33 +0100 Subject: [PATCH 1/2] ensure the API can poll OCI manifests --- CHANGELOG.md | 4 ++ docker-compose.yaml | 3 +- ...ing.java => ContainerRegistryPolling.java} | 8 ++-- .../oneko/docker/v2/DockerRegistryAPIV2.java | 2 +- .../docker/v2/DockerRegistryV2Client.java | 37 +++++++++++-------- .../manifest/DockerRegistryManifest.java | 13 +++++-- 6 files changed, 40 insertions(+), 27 deletions(-) rename src/main/java/io/oneko/docker/{DockerRegistryPolling.java => ContainerRegistryPolling.java} (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f04e86c1..fb29e987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.8.2 + +* fix: ensure the API can poll OCI manifests + ## 1.8.1 (2025-12-17) * prevent NullPointerExceptions when filling Immutable collections diff --git a/docker-compose.yaml b/docker-compose.yaml index c3bd0214..6e3abc7a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,6 @@ -version: '3' services: mongodb: - image: mongo:4 + image: mongo:8 container_name: o-neko-mongodb ports: - "27017:27017" \ No newline at end of file diff --git a/src/main/java/io/oneko/docker/DockerRegistryPolling.java b/src/main/java/io/oneko/docker/ContainerRegistryPolling.java similarity index 98% rename from src/main/java/io/oneko/docker/DockerRegistryPolling.java rename to src/main/java/io/oneko/docker/ContainerRegistryPolling.java index f14abd60..9bbec665 100644 --- a/src/main/java/io/oneko/docker/DockerRegistryPolling.java +++ b/src/main/java/io/oneko/docker/ContainerRegistryPolling.java @@ -48,9 +48,7 @@ @Component @Slf4j -class DockerRegistryPolling { - - +class ContainerRegistryPolling { @Data private static class VersionWithDockerManifest { @@ -71,7 +69,7 @@ private static class VersionWithDockerManifest { private final Timer pollingJobTimer; private final Timer updateDatesJobTimer; - DockerRegistryPolling(ProjectRepository projectRepository, + ContainerRegistryPolling(ProjectRepository projectRepository, DockerRegistryClientFactory dockerRegistryClientFactory, DeploymentManager deploymentManager, EventDispatcher eventDispatcher, @@ -237,7 +235,7 @@ private WritableProject manageAvailableVersions(WritableProject project, List defaultHeaders = new ArrayList<>(); defaultHeaders.add(new BasicHeader("Accept", "*/*")); @@ -86,9 +86,11 @@ public String versionCheck() { public List getAllTags(Project project) { final Timer.Sample sample = Timer.start(); try { - final List result = feignClient.getAllTags(project.getImageName()).getTags(); + final String imageName = project.getImageName(); + ListTagsResult result = feignClient.getAllTags(imageName); + List tags = result.getTags(); sample.stop(meters.getListAllTagsTimerOk()); - return result; + return tags; } catch (FeignException e) { sample.stop(meters.getListAllTagsTimerError()); log.warn("failed to list all container image tags ({})", kv("image_name", project.getImageName()), e); @@ -107,6 +109,10 @@ public Manifest getManifest(ProjectVersion version) { result = generateManifestFromManifestList(imageName, dockerRegistryManifest); } else { final DockerRegistryManifest.Digest digest = dockerRegistryManifest.getDigest(); + if (digest == null) { + log.warn("failed to get digest for project version ({}, {})", versionKv(version), projectKv(version.getProject())); + return null; + } final DockerRegistryBlob blob = feignClient.getBlob(imageName, digest.getAlgorithm(), digest.getDigest()); result = new Manifest(digest.getFullDigest(), blob.getCreated()); } @@ -135,7 +141,8 @@ public Manifest generateManifestFromManifestList(String imageName, DockerRegistr DockerRegistryBlob blob = feignClient.getBlob(imageName, m.getDigest().getAlgorithm(), m.getDigest().getDigest()); return new Manifest(m.getDigest().getFullDigest(), blob.getCreated()); }).reduce((m1, m2) -> { - String hash = "sha512:" + Hashing.sha512().hashString(m1.getDockerContentDigest() + m2.getDockerContentDigest(), StandardCharsets.UTF_8).toString(); + String hash = "sha512:" + Hashing.sha512().hashString(m1.getDockerContentDigest() + m2.getDockerContentDigest(), StandardCharsets.UTF_8) + .toString(); Instant date = m1.getImageUpdatedDate().map(d -> { if (m2.getImageUpdatedDate().isPresent() && d.isBefore(m2.getImageUpdatedDate().get())) { return m2.getImageUpdatedDate().get(); diff --git a/src/main/java/io/oneko/docker/v2/model/manifest/DockerRegistryManifest.java b/src/main/java/io/oneko/docker/v2/model/manifest/DockerRegistryManifest.java index 44573e9b..1dbcaf4a 100644 --- a/src/main/java/io/oneko/docker/v2/model/manifest/DockerRegistryManifest.java +++ b/src/main/java/io/oneko/docker/v2/model/manifest/DockerRegistryManifest.java @@ -10,7 +10,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class DockerRegistryManifest { @Data - static class Config { + public static class Config { String digest; String mediaType; int size; @@ -57,13 +57,18 @@ public String getFullDigest() { public Digest getDigest() { - if (!isManifestList()) { + if (isManifestList()) { + throw new IllegalStateException("tried to receive single digest from manifest list"); + } + if (config != null && config.digest != null) { return new Digest(config.digest); } - throw new IllegalStateException("tried to receive single digest from manifest list"); + return null; } public boolean isManifestList() { - return manifests != null && !manifests.isEmpty(); + return "application/vnd.oci.image.index.v1+json".equals(mediaType) || + "application/vnd.docker.distribution.manifest.list.v2+json".equals(mediaType) || + (manifests != null && !manifests.isEmpty()); } } From 18f58e7a59772756bac427ea4f2e49317d976224 Mon Sep 17 00:00:00 2001 From: Philip Dakowitz Date: Wed, 28 Jan 2026 16:40:06 +0100 Subject: [PATCH 2/2] refactor docker to container in the UI --- .../project-dashboard.component.html | 2 +- .../list/docker-registry-list.component.html | 4 ++-- frontend/src/assets/i18n/de.json | 22 ++++++++--------- frontend/src/assets/i18n/en.json | 24 +++++++++---------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/project/dashboard/project-dashboard.component.html b/frontend/src/app/project/dashboard/project-dashboard.component.html index a8ce9fa2..4d3c5e31 100644 --- a/frontend/src/app/project/dashboard/project-dashboard.component.html +++ b/frontend/src/app/project/dashboard/project-dashboard.component.html @@ -32,7 +32,7 @@

{{version.name}}

{{version.deployment.formattedTimestamp}} - {{version.formattedImageUpdatedDate}} + {{version.formattedImageUpdatedDate}}

diff --git a/frontend/src/app/registries/docker/list/docker-registry-list.component.html b/frontend/src/app/registries/docker/list/docker-registry-list.component.html index 44eba135..6b70934f 100644 --- a/frontend/src/app/registries/docker/list/docker-registry-list.component.html +++ b/frontend/src/app/registries/docker/list/docker-registry-list.component.html @@ -1,12 +1,12 @@

- + {{'components.dockerRegistry.dockerRegistries' | translate}}

diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index 46f9fbac..8ddcdec3 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -29,7 +29,7 @@ }, "administration": { "administration": "Administration", - "dockerRegistries": "Docker Registries", + "dockerRegistries": "Container Registries", "helmRegistries": "Helm Registries", "users": "Benutzer", "activityLog": "!alias:views.logs.activityLog" @@ -63,7 +63,7 @@ "activityLog": { "openEntity": "{entity} öffnen", "openUsersPage": "Benutzerliste öffnen", - "openDockerRegistryPage": "Docker-Registry-Liste öffnen", + "openDockerRegistryPage": "Container-Registry-Liste öffnen", "openNamespacesPage": "Namespace-Liste öffnen", "changedProperty": "Geänderter Wert", "newActivities": "{count} neue {count, plural, one{Event} other{Events}}" @@ -112,17 +112,17 @@ }, "dockerRegistry": { "deletionDialog": { - "registryIsUsedByProject": "Die Docker Registry {registry} wird von folgenden Projekten verwendet:", + "registryIsUsedByProject": "Die Container Registry {registry} wird von folgenden Projekten verwendet:", "usedRegistryWarningText": "

Wenn diese Registry gelöscht wird, verwaisen diese Projekte.

Sie können danach nicht mehr deployed oder anderweitig verwendet werden, solang ihnen keine neue Registry zugewiesen wird, die die Images unter dem selben Namen bereitstellt.

", "confirmDeletionText": "Bitte bestätigen Sie das Löschen der Registry indem Sie ihren Namen in das Eingabefeld tippen. Das Löschen kann nicht rückgängig gemacht werden.", "confirmName": "Namen der Registry bestätigen" }, "editDialog": { - "createRegistry": "Docker Registry anlegen", - "editRegistry": "Docker Registry bearbeiten", + "createRegistry": "Container Registry anlegen", + "editRegistry": "Container Registry bearbeiten", "trustInsecureCertificates": "Unsicheren Zertifikaten vertrauen", "trustInsecureRegistryHint": "Sie sollten diese Option nicht auswählen, wenn es sich um eine produktive Installation von O-Neko handelt. Stellen Sie lieber sicher, gültige und vertrauenswürdige Zertifikate in der Registry zu installieren. Sie müssen außerdem sicherstellen, dass Ihr Kubernetes Cluster dieser Registry vertraut, andernfalls wird auch das Auswählen dieser Option nicht helfen.", - "registryHasBeenModifiedByAction": "Die Docker Registry {registry} wurde {action, select, created{angelegt} deleted{gelöscht} saved{gespeichert} other{bearbeitet}}." + "registryHasBeenModifiedByAction": "Die Container Registry {registry} wurde {action, select, created{angelegt} deleted{gelöscht} saved{gespeichert} other{bearbeitet}}." }, "registryUrl": "Registry URL", "dockerRegistries": "!alias:menu.administration.dockerRegistries", @@ -230,11 +230,11 @@ "enterProjectNameDescription": "

Wählen Sie einen sinnvollen Namen für das Projekt.

Der Name kann beliebig gewählt werden, aber darf nicht mit bestehenden Namen kollidieren.

", "projectName": "Projektname", "collidingProjectNameMessage": "Es gibt bereits ein Projekt mit dem Namen {name}.", - "selectDockerRegistry": "Wählen Sie eine Docker Registry", - "selectDockerRegistryDescription": "Alle Docker Images des Projekts werden aus der angegebenen Docker Registry geladen.", - "dockerRegistry": "Docker Registry", + "selectDockerRegistry": "Wählen Sie eine Container Registry", + "selectDockerRegistryDescription": "Alle Container Images des Projekts werden aus der angegebenen Container Registry geladen.", + "dockerRegistry": "Container Registry", "enterProjectImageName": "Geben Sie den Image-Namen des Projekts ein", - "enterProjectImageNameDescription": "Der Name muss dem Namen entsprechen, den das Docker Image in der Docker Registry hat.", + "enterProjectImageNameDescription": "Der Name muss dem Namen entsprechen, den das Container Image in der Container Registry hat.", "couldNotUploadFile": "Die Datei {name} konnte nicht hochgeladen werden", "errorParsingConfiguration": "Ein Fehler ist beim Lesen der Konfiguration aufgetreten" }, @@ -250,7 +250,7 @@ "imageName": "Image-Name", "imageNameIsRequired": "Ein Image-Name ist erforderlich", "dockerRegistry": "!alias:components.project.createProjectDialog.dockerRegistry", - "dockerRegistryIsRequired": "Jedem Projekt muss eine Docker Registry zugewiesen sein", + "dockerRegistryIsRequired": "Jedem Projekt muss eine Container Registry zugewiesen sein", "namespaceInKubernetes": "Namespace in Kubernetes", "configurationTemplates": "Konfigurationstemplates", "templateVariables": "Template-Variablen", diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index f2573cf4..fec6c0a4 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -29,7 +29,7 @@ }, "administration": { "administration": "Administration", - "dockerRegistries": "Docker Registries", + "dockerRegistries": "Container Registries", "helmRegistries": "Helm Registries", "users": "Users", "activityLog": "!alias:views.logs.activityLog" @@ -63,7 +63,7 @@ "activityLog": { "openEntity": "Open {entity}", "openUsersPage": "Open user list", - "openDockerRegistryPage": "Open docker registry list", + "openDockerRegistryPage": "Open container registry list", "openNamespacesPage": "Open namespaces list", "changedProperty": "Changed property", "newActivities": "{count} new {count, plural, one{event} other{events}}" @@ -112,17 +112,17 @@ }, "dockerRegistry": { "deletionDialog": { - "registryIsUsedByProject": "The docker registry {registry} is used by these projects:", + "registryIsUsedByProject": "The contanier registry {registry} is used by these projects:", "usedRegistryWarningText": "

If you delete this registry then those projects will remain in an orphaned state.

They can no longer be deployed or used otherwise until they are assigned to a new registry serving images with the same name.

", - "confirmDeletionText": "Please confirm the deletion of this docker registry by entering its name below. This action cannot be undone.", + "confirmDeletionText": "Please confirm the deletion of this container registry by entering its name below. This action cannot be undone.", "confirmName": "!alias:components.forms.confirmName" }, "editDialog": { - "createRegistry": "Create Docker Registry", - "editRegistry": "Edit Docker Registry", + "createRegistry": "Create Container Registry", + "editRegistry": "Edit Container Registry", "trustInsecureCertificates": "Trust insecure certificates", "trustInsecureRegistryHint": "You should not check this if you are running O-Neko in production. Make sure to install valid and trusted certificates in your registry instead. You also have to make sure that your Kubernetes cluster trusts your registry. Otherwise even setting this option will not help you.", - "registryHasBeenModifiedByAction": "Docker Registry {registry} has been {action, select, created{created} deleted{deleted} saved{saved} other{modified}}." + "registryHasBeenModifiedByAction": "Container Registry {registry} has been {action, select, created{created} deleted{deleted} saved{saved} other{modified}}." }, "registryUrl": "Registry URL", "dockerRegistries": "!alias:menu.administration.dockerRegistries", @@ -230,11 +230,11 @@ "enterProjectNameDescription": "

Enter a meaningful name for your new project.

The name can be chosen arbitrarily but should be distinguishable from the names of yet existing projects.

", "projectName": "Project name", "collidingProjectNameMessage": "There is already a project with the name {name}.", - "selectDockerRegistry": "Select a docker registry", - "selectDockerRegistryDescription": "All docker images for this project will be picked from the docker registry you select here.", - "dockerRegistry": "Docker registry", + "selectDockerRegistry": "Select a container registry", + "selectDockerRegistryDescription": "All container images for this project will be picked from the container registry you select here.", + "dockerRegistry": "Container registry", "enterProjectImageName": "Enter the new project's image name", - "enterProjectImageNameDescription": "Type in the name of the docker image. This must match the image name as present in the docker registry.", + "enterProjectImageNameDescription": "Type in the name of the container image. This must match the image name as present in the container registry.", "couldNotUploadFile": "Could not upload file {name}", "errorParsingConfiguration": "Error while parsing the configuration file" }, @@ -250,7 +250,7 @@ "imageName": "Image name", "imageNameIsRequired": "An image name is required", "dockerRegistry": "!alias:components.project.createProjectDialog.dockerRegistry", - "dockerRegistryIsRequired": "Each project must have a docker registry assigned.", + "dockerRegistryIsRequired": "Each project must have a container registry assigned.", "namespaceInKubernetes": "Namespace in Kubernetes", "configurationTemplates": "Configuration templates", "templateVariables": "Template Variables",