Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 1 addition & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
version: '3'
services:
mongodb:
image: mongo:4
image: mongo:8
container_name: o-neko-mongodb
ports:
- "27017:27017"
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ <h4 mat-line>{{version.name}}</h4>
<p mat-line>
<span fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<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>
<span *ngIf="version.imageUpdatedDate" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px"><mat-icon svgIcon="mdi:docker"></mat-icon><span>{{version.formattedImageUpdatedDate}}</span></span>
<span *ngIf="version.imageUpdatedDate" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px"><mat-icon svgIcon="mdi:oci"></mat-icon><span>{{version.formattedImageUpdatedDate}}</span></span>
</span>
</p>
</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<div class="docker-registries table-page main-content-padding" fxLayout="column">
<h2 fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<mat-icon svgIcon="mdi:docker"></mat-icon>
<mat-icon svgIcon="mdi:oci"></mat-icon>
<span>{{'components.dockerRegistry.dockerRegistries' | translate}}</span>
</h2>
<div fxLayout="row" fxLayoutAlign="space-between center">
<button *ngIf="mayCreateDockerRegistry()" mat-button (click)="createDockerRegistry()">
<span fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="1em">
<mat-icon svgIcon="mdi:docker"></mat-icon>
<mat-icon svgIcon="mdi:oci"></mat-icon>
<span>{{'components.dockerRegistry.createDockerRegistry' | translate}}</span>
</span>
</button>
Expand Down
22 changes: 11 additions & 11 deletions frontend/src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"administration": {
"administration": "Administration",
"dockerRegistries": "Docker Registries",
"dockerRegistries": "Container Registries",
"helmRegistries": "Helm Registries",
"users": "Benutzer",
"activityLog": "!alias:views.logs.activityLog"
Expand Down Expand Up @@ -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}}"
Expand Down Expand Up @@ -112,17 +112,17 @@
},
"dockerRegistry": {
"deletionDialog": {
"registryIsUsedByProject": "Die Docker Registry <b>{registry}</b> wird von folgenden Projekten verwendet:",
"registryIsUsedByProject": "Die Container Registry <b>{registry}</b> wird von folgenden Projekten verwendet:",
"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>",
"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",
Expand Down Expand Up @@ -230,11 +230,11 @@
"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>",
"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"
},
Expand All @@ -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",
Expand Down
24 changes: 12 additions & 12 deletions frontend/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"administration": {
"administration": "Administration",
"dockerRegistries": "Docker Registries",
"dockerRegistries": "Container Registries",
"helmRegistries": "Helm Registries",
"users": "Users",
"activityLog": "!alias:views.logs.activityLog"
Expand Down Expand Up @@ -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}}"
Expand Down Expand Up @@ -112,17 +112,17 @@
},
"dockerRegistry": {
"deletionDialog": {
"registryIsUsedByProject": "The docker registry <b>{registry}</b> is used by these projects:",
"registryIsUsedByProject": "The contanier registry <b>{registry}</b> is used by these projects:",
"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>",
"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",
Expand Down Expand Up @@ -230,11 +230,11 @@
"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>",
"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"
},
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@

@Component
@Slf4j
class DockerRegistryPolling {


class ContainerRegistryPolling {

@Data
private static class VersionWithDockerManifest {
Expand All @@ -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,
Expand Down Expand Up @@ -237,7 +235,7 @@ private WritableProject manageAvailableVersions(WritableProject project, List<St
if (newVersions.size() == 1) {
log.info("found new project version ({}, {})", versionKv((String) newVersions.toArray()[0]), projectKv(project));
} else {
log.info("found new project versions ({}, {}, {})", kv("version_count", newVersions.size()), projectKv(project), kv("versions", newVersions));
log.info("found new project versions ({}, {})", kv("version_count", newVersions.size()), projectKv(project));
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/oneko/docker/v2/DockerRegistryAPIV2.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface DockerRegistryAPIV2 {

@RequestLine("GET /v2/{imageName}/manifests/{tagName}")
@Headers({
"Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json"
"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"
})
DockerRegistryManifest getManifest(@Param("imageName") String imageName, @Param("tagName") String tagName);

Expand Down
37 changes: 22 additions & 15 deletions src/main/java/io/oneko/docker/v2/DockerRegistryV2Client.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.oneko.docker.v2;

import static io.oneko.util.MoreStructuredArguments.projectKv;
import static io.oneko.util.MoreStructuredArguments.versionKv;
import static net.logstash.logback.argument.StructuredArguments.kv;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.hash.Hashing;
import feign.Feign;
Expand All @@ -11,11 +15,16 @@
import io.micrometer.core.instrument.Timer;
import io.oneko.docker.DockerRegistry;
import io.oneko.docker.v2.metrics.MetersPerRegistry;
import io.oneko.docker.v2.model.ListTagsResult;
import io.oneko.docker.v2.model.manifest.DockerRegistryBlob;
import io.oneko.docker.v2.model.manifest.DockerRegistryManifest;
import io.oneko.docker.v2.model.manifest.Manifest;
import io.oneko.project.Project;
import io.oneko.project.ProjectVersion;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.client.config.CookieSpecs;
Expand All @@ -24,15 +33,6 @@
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

import static io.oneko.util.MoreStructuredArguments.projectKv;
import static io.oneko.util.MoreStructuredArguments.versionKv;
import static net.logstash.logback.argument.StructuredArguments.kv;

/**
* Accesses the API defined here:
* https://docs.docker.com/registry/spec/api/
Expand All @@ -44,9 +44,9 @@ public class DockerRegistryV2Client {
private final MetersPerRegistry meters;

public DockerRegistryV2Client(DockerRegistry registry,
String token,
ObjectMapper objectMapper,
MetersPerRegistry meters) {
String token,
ObjectMapper objectMapper,
MetersPerRegistry meters) {
this.meters = meters;
List<Header> defaultHeaders = new ArrayList<>();
defaultHeaders.add(new BasicHeader("Accept", "*/*"));
Expand Down Expand Up @@ -86,9 +86,11 @@ public String versionCheck() {
public List<String> getAllTags(Project<?, ?> project) {
final Timer.Sample sample = Timer.start();
try {
final List<String> result = feignClient.getAllTags(project.getImageName()).getTags();
final String imageName = project.getImageName();
ListTagsResult result = feignClient.getAllTags(imageName);
List<String> 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);
Expand All @@ -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());
}
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@JsonIgnoreProperties(ignoreUnknown = true)
public class DockerRegistryManifest {
@Data
static class Config {
public static class Config {
String digest;
String mediaType;
int size;
Expand Down Expand Up @@ -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());
}
}