Skip to content

Commit d4059a2

Browse files
authored
ensure the API can poll OCI manifests (#383)
1 parent e7f2ad4 commit d4059a2

10 files changed

Lines changed: 66 additions & 53 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.8.2
2+
3+
* fix: ensure the API can poll OCI manifests
4+
15
## 1.8.1 (2025-12-17)
26

37
* prevent NullPointerExceptions when filling Immutable collections

docker-compose.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
version: '3'
21
services:
32
mongodb:
4-
image: mongo:4
3+
image: mongo:8
54
container_name: o-neko-mongodb
65
ports:
76
- "27017:27017"

frontend/src/app/project/dashboard/project-dashboard.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ <h4 mat-line>{{version.name}}</h4>
3232
<p mat-line>
3333
<span fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
3434
<span *ngIf="version.deployment.timestamp" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px"><mat-icon svgIcon="mdi:kubernetes"></mat-icon><span>{{version.deployment.formattedTimestamp}}</span></span>
35-
<span *ngIf="version.imageUpdatedDate" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px"><mat-icon svgIcon="mdi:docker"></mat-icon><span>{{version.formattedImageUpdatedDate}}</span></span>
35+
<span *ngIf="version.imageUpdatedDate" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px"><mat-icon svgIcon="mdi:oci"></mat-icon><span>{{version.formattedImageUpdatedDate}}</span></span>
3636
</span>
3737
</p>
3838
</a>

frontend/src/app/registries/docker/list/docker-registry-list.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<div class="docker-registries table-page main-content-padding" fxLayout="column">
22
<h2 fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
3-
<mat-icon svgIcon="mdi:docker"></mat-icon>
3+
<mat-icon svgIcon="mdi:oci"></mat-icon>
44
<span>{{'components.dockerRegistry.dockerRegistries' | translate}}</span>
55
</h2>
66
<div fxLayout="row" fxLayoutAlign="space-between center">
77
<button *ngIf="mayCreateDockerRegistry()" mat-button (click)="createDockerRegistry()">
88
<span fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="1em">
9-
<mat-icon svgIcon="mdi:docker"></mat-icon>
9+
<mat-icon svgIcon="mdi:oci"></mat-icon>
1010
<span>{{'components.dockerRegistry.createDockerRegistry' | translate}}</span>
1111
</span>
1212
</button>

frontend/src/assets/i18n/de.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
},
3030
"administration": {
3131
"administration": "Administration",
32-
"dockerRegistries": "Docker Registries",
32+
"dockerRegistries": "Container Registries",
3333
"helmRegistries": "Helm Registries",
3434
"users": "Benutzer",
3535
"activityLog": "!alias:views.logs.activityLog"
@@ -63,7 +63,7 @@
6363
"activityLog": {
6464
"openEntity": "{entity} öffnen",
6565
"openUsersPage": "Benutzerliste öffnen",
66-
"openDockerRegistryPage": "Docker-Registry-Liste öffnen",
66+
"openDockerRegistryPage": "Container-Registry-Liste öffnen",
6767
"openNamespacesPage": "Namespace-Liste öffnen",
6868
"changedProperty": "Geänderter Wert",
6969
"newActivities": "{count} neue {count, plural, one{Event} other{Events}}"
@@ -112,17 +112,17 @@
112112
},
113113
"dockerRegistry": {
114114
"deletionDialog": {
115-
"registryIsUsedByProject": "Die Docker Registry <b>{registry}</b> wird von folgenden Projekten verwendet:",
115+
"registryIsUsedByProject": "Die Container Registry <b>{registry}</b> wird von folgenden Projekten verwendet:",
116116
"usedRegistryWarningText": "<p>Wenn diese Registry gelöscht wird, verwaisen diese Projekte.</p><p>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.</p>",
117117
"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.",
118118
"confirmName": "Namen der Registry bestätigen"
119119
},
120120
"editDialog": {
121-
"createRegistry": "Docker Registry anlegen",
122-
"editRegistry": "Docker Registry bearbeiten",
121+
"createRegistry": "Container Registry anlegen",
122+
"editRegistry": "Container Registry bearbeiten",
123123
"trustInsecureCertificates": "Unsicheren Zertifikaten vertrauen",
124124
"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.",
125-
"registryHasBeenModifiedByAction": "Die Docker Registry {registry} wurde {action, select, created{angelegt} deleted{gelöscht} saved{gespeichert} other{bearbeitet}}."
125+
"registryHasBeenModifiedByAction": "Die Container Registry {registry} wurde {action, select, created{angelegt} deleted{gelöscht} saved{gespeichert} other{bearbeitet}}."
126126
},
127127
"registryUrl": "Registry URL",
128128
"dockerRegistries": "!alias:menu.administration.dockerRegistries",
@@ -230,11 +230,11 @@
230230
"enterProjectNameDescription": "<p>Wählen Sie einen sinnvollen Namen für das Projekt.</p><p>Der Name kann beliebig gewählt werden, aber darf nicht mit bestehenden Namen kollidieren.</p>",
231231
"projectName": "Projektname",
232232
"collidingProjectNameMessage": "Es gibt bereits ein Projekt mit dem Namen {name}.",
233-
"selectDockerRegistry": "Wählen Sie eine Docker Registry",
234-
"selectDockerRegistryDescription": "Alle Docker Images des Projekts werden aus der angegebenen Docker Registry geladen.",
235-
"dockerRegistry": "Docker Registry",
233+
"selectDockerRegistry": "Wählen Sie eine Container Registry",
234+
"selectDockerRegistryDescription": "Alle Container Images des Projekts werden aus der angegebenen Container Registry geladen.",
235+
"dockerRegistry": "Container Registry",
236236
"enterProjectImageName": "Geben Sie den Image-Namen des Projekts ein",
237-
"enterProjectImageNameDescription": "Der Name muss dem Namen entsprechen, den das Docker Image in der Docker Registry hat.",
237+
"enterProjectImageNameDescription": "Der Name muss dem Namen entsprechen, den das Container Image in der Container Registry hat.",
238238
"couldNotUploadFile": "Die Datei {name} konnte nicht hochgeladen werden",
239239
"errorParsingConfiguration": "Ein Fehler ist beim Lesen der Konfiguration aufgetreten"
240240
},
@@ -250,7 +250,7 @@
250250
"imageName": "Image-Name",
251251
"imageNameIsRequired": "Ein Image-Name ist erforderlich",
252252
"dockerRegistry": "!alias:components.project.createProjectDialog.dockerRegistry",
253-
"dockerRegistryIsRequired": "Jedem Projekt muss eine Docker Registry zugewiesen sein",
253+
"dockerRegistryIsRequired": "Jedem Projekt muss eine Container Registry zugewiesen sein",
254254
"namespaceInKubernetes": "Namespace in Kubernetes",
255255
"configurationTemplates": "Konfigurationstemplates",
256256
"templateVariables": "Template-Variablen",

frontend/src/assets/i18n/en.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
},
3030
"administration": {
3131
"administration": "Administration",
32-
"dockerRegistries": "Docker Registries",
32+
"dockerRegistries": "Container Registries",
3333
"helmRegistries": "Helm Registries",
3434
"users": "Users",
3535
"activityLog": "!alias:views.logs.activityLog"
@@ -63,7 +63,7 @@
6363
"activityLog": {
6464
"openEntity": "Open {entity}",
6565
"openUsersPage": "Open user list",
66-
"openDockerRegistryPage": "Open docker registry list",
66+
"openDockerRegistryPage": "Open container registry list",
6767
"openNamespacesPage": "Open namespaces list",
6868
"changedProperty": "Changed property",
6969
"newActivities": "{count} new {count, plural, one{event} other{events}}"
@@ -112,17 +112,17 @@
112112
},
113113
"dockerRegistry": {
114114
"deletionDialog": {
115-
"registryIsUsedByProject": "The docker registry <b>{registry}</b> is used by these projects:",
115+
"registryIsUsedByProject": "The contanier registry <b>{registry}</b> is used by these projects:",
116116
"usedRegistryWarningText": "<p>If you delete this registry then those projects will remain in an orphaned state.</p><p>They can no longer be deployed or used otherwise until they are assigned to a new registry serving images with the same name.</p>",
117-
"confirmDeletionText": "Please confirm the deletion of this docker registry by entering its name below. This action cannot be undone.",
117+
"confirmDeletionText": "Please confirm the deletion of this container registry by entering its name below. This action cannot be undone.",
118118
"confirmName": "!alias:components.forms.confirmName"
119119
},
120120
"editDialog": {
121-
"createRegistry": "Create Docker Registry",
122-
"editRegistry": "Edit Docker Registry",
121+
"createRegistry": "Create Container Registry",
122+
"editRegistry": "Edit Container Registry",
123123
"trustInsecureCertificates": "Trust insecure certificates",
124124
"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.",
125-
"registryHasBeenModifiedByAction": "Docker Registry {registry} has been {action, select, created{created} deleted{deleted} saved{saved} other{modified}}."
125+
"registryHasBeenModifiedByAction": "Container Registry {registry} has been {action, select, created{created} deleted{deleted} saved{saved} other{modified}}."
126126
},
127127
"registryUrl": "Registry URL",
128128
"dockerRegistries": "!alias:menu.administration.dockerRegistries",
@@ -230,11 +230,11 @@
230230
"enterProjectNameDescription": "<p>Enter a meaningful name for your new project.</p><p>The name can be chosen arbitrarily but should be distinguishable from the names of yet existing projects.</p>",
231231
"projectName": "Project name",
232232
"collidingProjectNameMessage": "There is already a project with the name {name}.",
233-
"selectDockerRegistry": "Select a docker registry",
234-
"selectDockerRegistryDescription": "All docker images for this project will be picked from the docker registry you select here.",
235-
"dockerRegistry": "Docker registry",
233+
"selectDockerRegistry": "Select a container registry",
234+
"selectDockerRegistryDescription": "All container images for this project will be picked from the container registry you select here.",
235+
"dockerRegistry": "Container registry",
236236
"enterProjectImageName": "Enter the new project's image name",
237-
"enterProjectImageNameDescription": "Type in the name of the docker image. This must match the image name as present in the docker registry.",
237+
"enterProjectImageNameDescription": "Type in the name of the container image. This must match the image name as present in the container registry.",
238238
"couldNotUploadFile": "Could not upload file {name}",
239239
"errorParsingConfiguration": "Error while parsing the configuration file"
240240
},
@@ -250,7 +250,7 @@
250250
"imageName": "Image name",
251251
"imageNameIsRequired": "An image name is required",
252252
"dockerRegistry": "!alias:components.project.createProjectDialog.dockerRegistry",
253-
"dockerRegistryIsRequired": "Each project must have a docker registry assigned.",
253+
"dockerRegistryIsRequired": "Each project must have a container registry assigned.",
254254
"namespaceInKubernetes": "Namespace in Kubernetes",
255255
"configurationTemplates": "Configuration templates",
256256
"templateVariables": "Template Variables",

src/main/java/io/oneko/docker/DockerRegistryPolling.java renamed to src/main/java/io/oneko/docker/ContainerRegistryPolling.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@
4848

4949
@Component
5050
@Slf4j
51-
class DockerRegistryPolling {
52-
53-
51+
class ContainerRegistryPolling {
5452

5553
@Data
5654
private static class VersionWithDockerManifest {
@@ -71,7 +69,7 @@ private static class VersionWithDockerManifest {
7169
private final Timer pollingJobTimer;
7270
private final Timer updateDatesJobTimer;
7371

74-
DockerRegistryPolling(ProjectRepository projectRepository,
72+
ContainerRegistryPolling(ProjectRepository projectRepository,
7573
DockerRegistryClientFactory dockerRegistryClientFactory,
7674
DeploymentManager deploymentManager,
7775
EventDispatcher eventDispatcher,
@@ -237,7 +235,7 @@ private WritableProject manageAvailableVersions(WritableProject project, List<St
237235
if (newVersions.size() == 1) {
238236
log.info("found new project version ({}, {})", versionKv((String) newVersions.toArray()[0]), projectKv(project));
239237
} else {
240-
log.info("found new project versions ({}, {}, {})", kv("version_count", newVersions.size()), projectKv(project), kv("versions", newVersions));
238+
log.info("found new project versions ({}, {})", kv("version_count", newVersions.size()), projectKv(project));
241239
}
242240
}
243241

src/main/java/io/oneko/docker/v2/DockerRegistryAPIV2.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public interface DockerRegistryAPIV2 {
1717

1818
@RequestLine("GET /v2/{imageName}/manifests/{tagName}")
1919
@Headers({
20-
"Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json"
20+
"Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json"
2121
})
2222
DockerRegistryManifest getManifest(@Param("imageName") String imageName, @Param("tagName") String tagName);
2323

src/main/java/io/oneko/docker/v2/DockerRegistryV2Client.java

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package io.oneko.docker.v2;
22

3+
import static io.oneko.util.MoreStructuredArguments.projectKv;
4+
import static io.oneko.util.MoreStructuredArguments.versionKv;
5+
import static net.logstash.logback.argument.StructuredArguments.kv;
6+
37
import com.fasterxml.jackson.databind.ObjectMapper;
48
import com.google.common.hash.Hashing;
59
import feign.Feign;
@@ -11,11 +15,16 @@
1115
import io.micrometer.core.instrument.Timer;
1216
import io.oneko.docker.DockerRegistry;
1317
import io.oneko.docker.v2.metrics.MetersPerRegistry;
18+
import io.oneko.docker.v2.model.ListTagsResult;
1419
import io.oneko.docker.v2.model.manifest.DockerRegistryBlob;
1520
import io.oneko.docker.v2.model.manifest.DockerRegistryManifest;
1621
import io.oneko.docker.v2.model.manifest.Manifest;
1722
import io.oneko.project.Project;
1823
import io.oneko.project.ProjectVersion;
24+
import java.nio.charset.StandardCharsets;
25+
import java.time.Instant;
26+
import java.util.ArrayList;
27+
import java.util.List;
1928
import lombok.extern.slf4j.Slf4j;
2029
import org.apache.http.Header;
2130
import org.apache.http.client.config.CookieSpecs;
@@ -24,15 +33,6 @@
2433
import org.apache.http.impl.client.HttpClients;
2534
import org.apache.http.message.BasicHeader;
2635

27-
import java.nio.charset.StandardCharsets;
28-
import java.time.Instant;
29-
import java.util.ArrayList;
30-
import java.util.List;
31-
32-
import static io.oneko.util.MoreStructuredArguments.projectKv;
33-
import static io.oneko.util.MoreStructuredArguments.versionKv;
34-
import static net.logstash.logback.argument.StructuredArguments.kv;
35-
3636
/**
3737
* Accesses the API defined here:
3838
* https://docs.docker.com/registry/spec/api/
@@ -44,9 +44,9 @@ public class DockerRegistryV2Client {
4444
private final MetersPerRegistry meters;
4545

4646
public DockerRegistryV2Client(DockerRegistry registry,
47-
String token,
48-
ObjectMapper objectMapper,
49-
MetersPerRegistry meters) {
47+
String token,
48+
ObjectMapper objectMapper,
49+
MetersPerRegistry meters) {
5050
this.meters = meters;
5151
List<Header> defaultHeaders = new ArrayList<>();
5252
defaultHeaders.add(new BasicHeader("Accept", "*/*"));
@@ -86,9 +86,11 @@ public String versionCheck() {
8686
public List<String> getAllTags(Project<?, ?> project) {
8787
final Timer.Sample sample = Timer.start();
8888
try {
89-
final List<String> result = feignClient.getAllTags(project.getImageName()).getTags();
89+
final String imageName = project.getImageName();
90+
ListTagsResult result = feignClient.getAllTags(imageName);
91+
List<String> tags = result.getTags();
9092
sample.stop(meters.getListAllTagsTimerOk());
91-
return result;
93+
return tags;
9294
} catch (FeignException e) {
9395
sample.stop(meters.getListAllTagsTimerError());
9496
log.warn("failed to list all container image tags ({})", kv("image_name", project.getImageName()), e);
@@ -107,6 +109,10 @@ public Manifest getManifest(ProjectVersion<?, ?> version) {
107109
result = generateManifestFromManifestList(imageName, dockerRegistryManifest);
108110
} else {
109111
final DockerRegistryManifest.Digest digest = dockerRegistryManifest.getDigest();
112+
if (digest == null) {
113+
log.warn("failed to get digest for project version ({}, {})", versionKv(version), projectKv(version.getProject()));
114+
return null;
115+
}
110116
final DockerRegistryBlob blob = feignClient.getBlob(imageName, digest.getAlgorithm(), digest.getDigest());
111117
result = new Manifest(digest.getFullDigest(), blob.getCreated());
112118
}
@@ -135,7 +141,8 @@ public Manifest generateManifestFromManifestList(String imageName, DockerRegistr
135141
DockerRegistryBlob blob = feignClient.getBlob(imageName, m.getDigest().getAlgorithm(), m.getDigest().getDigest());
136142
return new Manifest(m.getDigest().getFullDigest(), blob.getCreated());
137143
}).reduce((m1, m2) -> {
138-
String hash = "sha512:" + Hashing.sha512().hashString(m1.getDockerContentDigest() + m2.getDockerContentDigest(), StandardCharsets.UTF_8).toString();
144+
String hash = "sha512:" + Hashing.sha512().hashString(m1.getDockerContentDigest() + m2.getDockerContentDigest(), StandardCharsets.UTF_8)
145+
.toString();
139146
Instant date = m1.getImageUpdatedDate().map(d -> {
140147
if (m2.getImageUpdatedDate().isPresent() && d.isBefore(m2.getImageUpdatedDate().get())) {
141148
return m2.getImageUpdatedDate().get();

src/main/java/io/oneko/docker/v2/model/manifest/DockerRegistryManifest.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
@JsonIgnoreProperties(ignoreUnknown = true)
1111
public class DockerRegistryManifest {
1212
@Data
13-
static class Config {
13+
public static class Config {
1414
String digest;
1515
String mediaType;
1616
int size;
@@ -57,13 +57,18 @@ public String getFullDigest() {
5757

5858

5959
public Digest getDigest() {
60-
if (!isManifestList()) {
60+
if (isManifestList()) {
61+
throw new IllegalStateException("tried to receive single digest from manifest list");
62+
}
63+
if (config != null && config.digest != null) {
6164
return new Digest(config.digest);
6265
}
63-
throw new IllegalStateException("tried to receive single digest from manifest list");
66+
return null;
6467
}
6568

6669
public boolean isManifestList() {
67-
return manifests != null && !manifests.isEmpty();
70+
return "application/vnd.oci.image.index.v1+json".equals(mediaType) ||
71+
"application/vnd.docker.distribution.manifest.list.v2+json".equals(mediaType) ||
72+
(manifests != null && !manifests.isEmpty());
6873
}
6974
}

0 commit comments

Comments
 (0)