From c533dff00e939d8cb4f74a080d2f3aacd67fd697 Mon Sep 17 00:00:00 2001 From: bethibande Date: Wed, 11 Feb 2026 19:52:05 +0100 Subject: [PATCH 01/29] Kubernetes Gateway API support for routing #36 --- docker-compose.yaml | 2 +- server/build.gradle.kts | 2 + .../jpa/repository/PackageManager.java | 5 +- .../repository/jpa/repository/Repository.java | 11 +- .../jpa/repository/RepositoryFactory.java | 4 +- .../jpa/repository/RepositoryManager.java | 15 +- .../permissions/PermissionScope.java | 4 +- .../repository/ManagedRepository.java | 1 + .../repository/backend/S3Backend.java | 2 + .../repository/maven/MavenRepository.java | 8 +- .../repository/oci/OCIRepository.java | 156 ++++++++++++------ .../repository/oci/OCIRepositoryConfig.java | 12 -- .../oci/details/OCILayerReference.java | 3 + .../oci/details/OCIManifestDetails.java | 3 + .../oci/details/OCIManifestReference.java | 3 + .../web/api/RepositoryEndpoint.java | 28 +++- .../repositories/OCIRepositoryEndpoint.java | 38 +++-- .../components/repository/OCIConfigForm.tsx | 126 ++++++++++++-- .../src/view/dashboard/RepositoryEditView.tsx | 27 ++- 19 files changed, 335 insertions(+), 115 deletions(-) delete mode 100644 server/src/main/java/com/bethibande/repository/repository/oci/OCIRepositoryConfig.java diff --git a/docker-compose.yaml b/docker-compose.yaml index 039d368..86bf743 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,7 +33,7 @@ services: volumes: - minio:/data repository: - image: bethibande/repository:1.0 + image: maxbb/server:1.0-SNAPSHOT restart: unless-stopped ports: - "8080:8080" diff --git a/server/build.gradle.kts b/server/build.gradle.kts index fa3d9ed..c3c31cd 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -39,10 +39,12 @@ dependencies { implementation("com.cronutils:cron-utils:9.2.1") implementation("software.amazon.awssdk:s3:2.41.24") + implementation("software.amazon.awssdk:apache-client:2.41.26") implementation("io.hypersistence:hypersistence-utils-hibernate-71:3.14.1") implementation("com.bethibande.process:annotations:1.5") + implementation("io.quarkus:quarkus-kubernetes-client") annotationProcessor("com.bethibande.process:processor:1.5") // Jackson & Hibernate Search ORM diff --git a/server/src/main/java/com/bethibande/repository/jpa/repository/PackageManager.java b/server/src/main/java/com/bethibande/repository/jpa/repository/PackageManager.java index 916d764..35277e6 100644 --- a/server/src/main/java/com/bethibande/repository/jpa/repository/PackageManager.java +++ b/server/src/main/java/com/bethibande/repository/jpa/repository/PackageManager.java @@ -1,6 +1,7 @@ package com.bethibande.repository.jpa.repository; import com.bethibande.repository.repository.ManagedRepository; +import com.bethibande.repository.repository.RepositoryApplicationContext; import com.bethibande.repository.repository.maven.MavenRepository; import com.bethibande.repository.repository.oci.OCIRepository; import com.fasterxml.jackson.core.JsonProcessingException; @@ -17,9 +18,9 @@ public enum PackageManager { this.factory = factory; } - public ManagedRepository createRepository(final Repository info, final ObjectMapper mapper) { + public ManagedRepository createRepository(final Repository info, final RepositoryApplicationContext ctx) { try { - return factory.createRepository(info, mapper); + return factory.createRepository(info, ctx); } catch (final JsonProcessingException ex) { throw new RuntimeException(ex); } diff --git a/server/src/main/java/com/bethibande/repository/jpa/repository/Repository.java b/server/src/main/java/com/bethibande/repository/jpa/repository/Repository.java index d8c3913..4c4b613 100644 --- a/server/src/main/java/com/bethibande/repository/jpa/repository/Repository.java +++ b/server/src/main/java/com/bethibande/repository/jpa/repository/Repository.java @@ -53,8 +53,17 @@ public T getMetadata(final RepositoryMetadataKey key) { return (T) metadata.get(key); } + public T getMetadataOrDefault(final RepositoryMetadataKey key, final T defaultValue) { + final T value = getMetadata(key); + return value != null ? value : defaultValue; + } + public void setMetadata(final RepositoryMetadataKey key, final Object value) { - metadata.put(key, value); + if (value == null) { + metadata.remove(key); + } else { + metadata.put(key, value); + } } public boolean canView(final User user) { diff --git a/server/src/main/java/com/bethibande/repository/jpa/repository/RepositoryFactory.java b/server/src/main/java/com/bethibande/repository/jpa/repository/RepositoryFactory.java index b325589..fa49cb6 100644 --- a/server/src/main/java/com/bethibande/repository/jpa/repository/RepositoryFactory.java +++ b/server/src/main/java/com/bethibande/repository/jpa/repository/RepositoryFactory.java @@ -1,12 +1,14 @@ package com.bethibande.repository.jpa.repository; import com.bethibande.repository.repository.ManagedRepository; +import com.bethibande.repository.repository.RepositoryApplicationContext; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @FunctionalInterface public interface RepositoryFactory { - ManagedRepository createRepository(final Repository info, final ObjectMapper mapper) throws JsonProcessingException; + ManagedRepository createRepository(final Repository info, + final RepositoryApplicationContext ctx) throws JsonProcessingException; } diff --git a/server/src/main/java/com/bethibande/repository/jpa/repository/RepositoryManager.java b/server/src/main/java/com/bethibande/repository/jpa/repository/RepositoryManager.java index 226df41..4a9e21a 100644 --- a/server/src/main/java/com/bethibande/repository/jpa/repository/RepositoryManager.java +++ b/server/src/main/java/com/bethibande/repository/jpa/repository/RepositoryManager.java @@ -1,7 +1,10 @@ package com.bethibande.repository.jpa.repository; +import com.bethibande.repository.k8s.KubernetesSupport; import com.bethibande.repository.repository.ManagedRepository; +import com.bethibande.repository.repository.RepositoryApplicationContext; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -11,6 +14,16 @@ public class RepositoryManager { @Inject protected ObjectMapper mapper; + @Inject + protected KubernetesSupport kubernetesSupport; + + private RepositoryApplicationContext ctx; + + @PostConstruct + protected void init() { + ctx = new RepositoryApplicationContext(mapper, kubernetesSupport); + } + public T findRepository(final String name, final PackageManager packageManager) { final Repository repo = Repository.find("name = ?1 and packageManager = ?2", name, packageManager).firstResult(); if (repo == null) return null; @@ -20,7 +33,7 @@ public T findRepository(final String name, final P @SuppressWarnings("unchecked") public T manage(final Repository repo) { - return (T) repo.packageManager.createRepository(repo, mapper); + return (T) repo.packageManager.createRepository(repo, this.ctx); } } diff --git a/server/src/main/java/com/bethibande/repository/jpa/repository/permissions/PermissionScope.java b/server/src/main/java/com/bethibande/repository/jpa/repository/permissions/PermissionScope.java index b75a98b..7c8cd30 100644 --- a/server/src/main/java/com/bethibande/repository/jpa/repository/permissions/PermissionScope.java +++ b/server/src/main/java/com/bethibande/repository/jpa/repository/permissions/PermissionScope.java @@ -37,7 +37,7 @@ public boolean canView(final User user) { return switch (type) { case ANONYMOUS -> true; case AUTHENTICATED -> user != null; - case USER -> Objects.equals(this.user.id, user.id); + case USER -> user != null && Objects.equals(this.user.id, user.id); }; } @@ -47,7 +47,7 @@ public boolean canWrite(final User user) { return switch (type) { case ANONYMOUS -> true; case AUTHENTICATED -> user != null; - case USER -> Objects.equals(this.user.id, user.id); + case USER -> user != null && Objects.equals(this.user.id, user.id); }; } diff --git a/server/src/main/java/com/bethibande/repository/repository/ManagedRepository.java b/server/src/main/java/com/bethibande/repository/repository/ManagedRepository.java index f086209..252384a 100644 --- a/server/src/main/java/com/bethibande/repository/repository/ManagedRepository.java +++ b/server/src/main/java/com/bethibande/repository/repository/ManagedRepository.java @@ -4,6 +4,7 @@ import com.bethibande.repository.jpa.artifact.ArtifactVersion; import com.bethibande.repository.jpa.repository.Repository; import com.bethibande.repository.jpa.user.User; +import jakarta.persistence.EntityManager; import org.hibernate.search.mapper.orm.Search; import org.hibernate.search.mapper.orm.session.SearchSession; diff --git a/server/src/main/java/com/bethibande/repository/repository/backend/S3Backend.java b/server/src/main/java/com/bethibande/repository/repository/backend/S3Backend.java index a7487ed..6c3558b 100644 --- a/server/src/main/java/com/bethibande/repository/repository/backend/S3Backend.java +++ b/server/src/main/java/com/bethibande/repository/repository/backend/S3Backend.java @@ -7,6 +7,7 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.http.apache.ApacheHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; @@ -25,6 +26,7 @@ public class S3Backend implements RepositoryBackend { public S3Backend(final S3Config config) { this.config = config; this.client = S3Client.builder() + .httpClientBuilder(ApacheHttpClient.builder()) .endpointOverride(URI.create(config.url())) .region(Region.of(config.region())) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(config.accessKey(), config.secretKey()))) diff --git a/server/src/main/java/com/bethibande/repository/repository/maven/MavenRepository.java b/server/src/main/java/com/bethibande/repository/repository/maven/MavenRepository.java index b83fcbe..4e01b3a 100644 --- a/server/src/main/java/com/bethibande/repository/repository/maven/MavenRepository.java +++ b/server/src/main/java/com/bethibande/repository/repository/maven/MavenRepository.java @@ -1,16 +1,16 @@ package com.bethibande.repository.repository.maven; -import com.bethibande.repository.jpa.files.StoredFile; import com.bethibande.repository.jpa.artifact.Artifact; import com.bethibande.repository.jpa.artifact.ArtifactVersion; +import com.bethibande.repository.jpa.files.StoredFile; import com.bethibande.repository.jpa.repository.Repository; import com.bethibande.repository.jpa.user.User; import com.bethibande.repository.repository.ManagedRepository; +import com.bethibande.repository.repository.RepositoryApplicationContext; import com.bethibande.repository.repository.StreamHandle; import com.bethibande.repository.repository.backend.RepositoryBackend; import com.bethibande.repository.repository.backend.S3Backend; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.security.UnauthorizedException; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.NotFoundException; @@ -43,8 +43,8 @@ public class MavenRepository implements ManagedRepository { private final RepositoryBackend backend; - public MavenRepository(final Repository info, final ObjectMapper mapper) throws JsonProcessingException { - this(info, mapper.readValue(info.settings, MavenRepositoryConfig.class)); + public MavenRepository(final Repository info, final RepositoryApplicationContext ctx) throws JsonProcessingException { + this(info, ctx.objectMapper().readValue(info.settings, MavenRepositoryConfig.class)); } public MavenRepository(final Repository info, final MavenRepositoryConfig config) { diff --git a/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepository.java b/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepository.java index 137006e..991ce19 100644 --- a/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepository.java +++ b/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepository.java @@ -6,13 +6,15 @@ import com.bethibande.repository.jpa.files.OCISubject; import com.bethibande.repository.jpa.files.StoredFile; import com.bethibande.repository.jpa.repository.Repository; +import com.bethibande.repository.jpa.repository.RepositoryMetadataKey; import com.bethibande.repository.jpa.user.User; -import com.bethibande.repository.repository.ArtifactAndGroupId; -import com.bethibande.repository.repository.ManagedRepository; -import com.bethibande.repository.repository.StreamHandle; +import com.bethibande.repository.k8s.KubernetesSupport; +import com.bethibande.repository.repository.*; import com.bethibande.repository.repository.backend.MultipartUploadStatus; import com.bethibande.repository.repository.backend.ObjectInfo; import com.bethibande.repository.repository.backend.S3Backend; +import com.bethibande.repository.repository.oci.config.OCIRepositoryConfig; +import com.bethibande.repository.repository.oci.config.OCIRoutingConfig; import com.bethibande.repository.repository.oci.details.OCILayerReference; import com.bethibande.repository.repository.oci.details.OCIManifestDetails; import com.bethibande.repository.repository.oci.details.OCIManifestReference; @@ -22,6 +24,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.narayana.jta.TransactionSemantics; import io.quarkus.security.UnauthorizedException; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.ForbiddenException; @@ -41,7 +45,7 @@ import java.time.Instant; import java.util.*; -public class OCIRepository implements ManagedRepository { +public class OCIRepository implements ManagedRepository, RepositoryUpdatedNotifier { public static final long MAX_MANIFEST_SIZE = 10_000_000L; @@ -49,20 +53,50 @@ public class OCIRepository implements ManagedRepository { private final Repository info; private final OCIRepositoryConfig config; + private final KubernetesSupport kubernetesSupport; private final S3Backend backend; - public OCIRepository(final Repository info, final ObjectMapper mapper) throws JsonProcessingException { - this(info, mapper.readValue(info.settings, OCIRepositoryConfig.class)); + public OCIRepository(final Repository info, final RepositoryApplicationContext ctx) throws JsonProcessingException { + this(info, ctx.objectMapper().readValue(info.settings, OCIRepositoryConfig.class), ctx.kubernetesSupport()); } - public OCIRepository(final Repository info, final OCIRepositoryConfig config) { + public OCIRepository(final Repository info, + final OCIRepositoryConfig config, + final KubernetesSupport kubernetesSupport) { this.info = info; this.config = config; + this.kubernetesSupport = kubernetesSupport; this.backend = new S3Backend(config.s3Config()); } + @Override + public void processUpdate(final UpdateType type) { + final OCIRoutingConfig routing = this.config.routingConfig(); + + if (!this.kubernetesSupport.hasHttpRouteSupport()) return; + + if (routing.enabled() && type != UpdateType.DELETE) { + this.kubernetesSupport.createOrUpdateRepositoryHttpRoute( + info.name, + info.packageManager, + info.getMetadata(RepositoryMetadataKey.HOST_NAME), + routing.targetService(), + routing.targetPort(), + routing.gatewayName(), + routing.gatewayNamespace() + ); + } + if (!routing.enabled() || type == UpdateType.DELETE) { + this.kubernetesSupport.deleteRepositoryHttpRouteIfExists( + info.name, + info.packageManager + ); + } + info.persist(); + } + @Override public Repository getInfo() { return info; @@ -165,13 +199,15 @@ protected StoredFile findManifestFileByTag(final String namespace, final String final String groupId = artifactAndGroupId.groupId(); final String artifactId = artifactAndGroupId.artifactId(); - final Artifact artifact = Artifact.find("groupId = ?1 and artifactId = ?2 and repository.id = ?3", groupId, artifactId, info.id).firstResult(); - if (artifact == null) return null; + return QuarkusTransaction.runner(TransactionSemantics.JOIN_EXISTING).call(() -> { + final Artifact artifact = Artifact.find("groupId = ?1 and artifactId = ?2 and repository.id = ?3", groupId, artifactId, info.id).firstResult(); + if (artifact == null) return null; - final ArtifactVersion version = ArtifactVersion.find("artifact = ?1 and version = ?2", artifact, tag).firstResult(); - if (version == null) return null; + final ArtifactVersion version = ArtifactVersion.find("artifact = ?1 and version = ?2", artifact, tag).firstResult(); + if (version == null) return null; - return version.manifest; + return version.manifest; + }); } protected boolean isDigest(final String reference) { @@ -219,6 +255,7 @@ public OCIContentInfo getManifestInfo(final User user, final String namespace, f final String digest = file.key.substring(file.key.lastIndexOf('/') + 1); final ObjectInfo info = this.backend.headObject(file.key); + if (info == null) return null; return new OCIContentInfo( digest, @@ -253,7 +290,23 @@ public StreamHandle getBlob(final User user, public void uploadBlob(final User user, final String namespace, final String digest, final StreamHandle stream) { checkWriteAccess(user); - this.backend.put(toBlobKey(namespace, digest), stream); + final String key = toBlobKey(namespace, digest); + this.backend.put(key, stream); + + QuarkusTransaction.runner(TransactionSemantics.JOIN_EXISTING).run(() -> { + final String[] digestParts = digest.split(":"); + + final Instant now = Instant.now(); + final StoredFile file = new StoredFile(); + file.key = key; + file.repository = info; + file.created = now; + file.updated = now; + file.contentType = stream.contentType(); + file.contentLength = stream.contentLength(); + file.hashes = Map.of(digestParts[0], digestParts[1]); + file.persist(); + }); } public UploadSessionHandle startUploadSession(final User user, final String namespace) { @@ -335,15 +388,18 @@ public void completeUploadSession(final User user, final ObjectInfo blobInfo = this.backend.headObject(blobKey); - final StoredFile file = new StoredFile(); - file.key = toBlobKey(namespace, digest); - file.repository = info; - file.created = Instant.now(); - file.updated = Instant.now(); - file.contentType = "application/octet-stream"; - file.contentLength = blobInfo.contentLength(); - file.hashes = Map.of(algorithm, hash); - file.persist(); + QuarkusTransaction.runner(TransactionSemantics.JOIN_EXISTING).run(() -> { + final StoredFile file = new StoredFile(); + file.key = toBlobKey(namespace, digest); + file.repository = info; + file.created = Instant.now(); + file.updated = Instant.now(); + file.contentType = "application/octet-stream"; + file.contentLength = blobInfo.contentLength(); + file.hashes = Map.of(algorithm, hash); + file.persist(); + }); + } public void abortUpload(final User user, final String uploadId, final String namespace, final UUID sessionId) { @@ -635,38 +691,40 @@ public PutOCIManifestResult putManifest(final User user, final String fileKey = toManifestKey(namespace, "sha256:" + hash); putFile(fileKey, contents, stream.contentType()); - final Instant now = Instant.now(); - final StoredFile file = storeOrUpdateFileReference( - fileKey, - now, - hash, - stream.contentType(), - stream.contentLength() - ); - final OCISubject subject = createSubjectInfo(namespace, file, contents); + return QuarkusTransaction.runner(TransactionSemantics.JOIN_EXISTING).call(() -> { + final Instant now = Instant.now(); + final StoredFile file = storeOrUpdateFileReference( + fileKey, + now, + hash, + stream.contentType(), + stream.contentLength() + ); + final OCISubject subject = createSubjectInfo(namespace, file, contents); - if (!isDigest(reference)) { - final ArtifactVersion version = updateOrCreateArtifactAndVersion(now, namespace, reference); - version.files.add(file); - version.manifest = file; + if (!isDigest(reference)) { + final ArtifactVersion version = updateOrCreateArtifactAndVersion(now, namespace, reference); + version.files.add(file); + version.manifest = file; - try { - version.details = parseDetails(contents); + try { + version.details = parseDetails(contents); - if (version.details.additionalData() instanceof OCIManifestDetails manifestDetails) { - version.files.addAll(collectAdditionalFileReferences(namespace, manifestDetails)); + if (version.details.additionalData() instanceof OCIManifestDetails manifestDetails) { + version.files.addAll(collectAdditionalFileReferences(namespace, manifestDetails)); + } + } catch (final IOException ex) { + throw new RuntimeException(ex); } - } catch (final IOException ex) { - throw new RuntimeException(ex); - } - return new PutOCIManifestResult( - file, - version, - subject - ); - } + return new PutOCIManifestResult( + file, + version, + subject + ); + } - return new PutOCIManifestResult(file, null, subject); + return new PutOCIManifestResult(file, null, subject); + }); } } diff --git a/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepositoryConfig.java b/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepositoryConfig.java deleted file mode 100644 index 1b77bbc..0000000 --- a/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepositoryConfig.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.bethibande.repository.repository.oci; - -import com.bethibande.repository.repository.S3Config; -import io.quarkus.runtime.annotations.RegisterForReflection; -import jakarta.validation.constraints.NotNull; - -@RegisterForReflection -public record OCIRepositoryConfig( - @NotNull - S3Config s3Config -) { -} diff --git a/server/src/main/java/com/bethibande/repository/repository/oci/details/OCILayerReference.java b/server/src/main/java/com/bethibande/repository/repository/oci/details/OCILayerReference.java index bccf646..563c25f 100644 --- a/server/src/main/java/com/bethibande/repository/repository/oci/details/OCILayerReference.java +++ b/server/src/main/java/com/bethibande/repository/repository/oci/details/OCILayerReference.java @@ -1,5 +1,8 @@ package com.bethibande.repository.repository.oci.details; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection public record OCILayerReference( String digest ) { diff --git a/server/src/main/java/com/bethibande/repository/repository/oci/details/OCIManifestDetails.java b/server/src/main/java/com/bethibande/repository/repository/oci/details/OCIManifestDetails.java index 3f08402..a16db52 100644 --- a/server/src/main/java/com/bethibande/repository/repository/oci/details/OCIManifestDetails.java +++ b/server/src/main/java/com/bethibande/repository/repository/oci/details/OCIManifestDetails.java @@ -1,7 +1,10 @@ package com.bethibande.repository.repository.oci.details; +import io.quarkus.runtime.annotations.RegisterForReflection; + import java.util.List; +@RegisterForReflection public record OCIManifestDetails( String configDigest, List manifests, diff --git a/server/src/main/java/com/bethibande/repository/repository/oci/details/OCIManifestReference.java b/server/src/main/java/com/bethibande/repository/repository/oci/details/OCIManifestReference.java index 4862ba5..76b8ce9 100644 --- a/server/src/main/java/com/bethibande/repository/repository/oci/details/OCIManifestReference.java +++ b/server/src/main/java/com/bethibande/repository/repository/oci/details/OCIManifestReference.java @@ -1,5 +1,8 @@ package com.bethibande.repository.repository.oci.details; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection public record OCIManifestReference( String digest, String architecture, diff --git a/server/src/main/java/com/bethibande/repository/web/api/RepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/api/RepositoryEndpoint.java index dccf3e1..f543792 100644 --- a/server/src/main/java/com/bethibande/repository/web/api/RepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/api/RepositoryEndpoint.java @@ -2,14 +2,14 @@ import com.bethibande.repository.jpa.artifact.Artifact; import com.bethibande.repository.jpa.artifact.ArtifactVersion; -import com.bethibande.repository.jpa.repository.PublicRepositoryDTO; -import com.bethibande.repository.jpa.repository.Repository; -import com.bethibande.repository.jpa.repository.RepositoryDTO; -import com.bethibande.repository.jpa.repository.RepositoryDTOWithoutId; +import com.bethibande.repository.jpa.repository.*; import com.bethibande.repository.jpa.repository.permissions.PermissionScope; import com.bethibande.repository.jpa.repository.permissions.UserSelectionType; import com.bethibande.repository.jpa.user.User; import com.bethibande.repository.jpa.user.UserRole; +import com.bethibande.repository.repository.ManagedRepository; +import com.bethibande.repository.repository.RepositoryUpdatedNotifier; +import com.bethibande.repository.repository.UpdateType; import com.bethibande.repository.web.AuthenticatedUser; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; @@ -31,10 +31,19 @@ public class RepositoryEndpoint { private final AuthenticatedUser authenticatedUser; + private final RepositoryManager repositoryManager; @Inject - public RepositoryEndpoint(final AuthenticatedUser authenticatedUser) { + public RepositoryEndpoint(final AuthenticatedUser authenticatedUser, final RepositoryManager repositoryManager) { this.authenticatedUser = authenticatedUser; + this.repositoryManager = repositoryManager; + } + + protected void processUpdate(final Repository entity, final UpdateType type) { + final ManagedRepository managed = repositoryManager.manage(entity); + if (managed instanceof RepositoryUpdatedNotifier notifier) { + notifier.processUpdate(type); + } } @POST @@ -49,6 +58,8 @@ public RepositoryDTO create(final RepositoryDTOWithoutId dto) { repository.persist(); + processUpdate(repository, UpdateType.CREATE); + return RepositoryDTO.from(repository); } @@ -64,6 +75,8 @@ public RepositoryDTO update(final RepositoryDTO dto) { repository.cleanupPolicies = dto.cleanupPolicies(); repository.persist(); + processUpdate(repository, UpdateType.UPDATE); + return RepositoryDTO.from(repository); } @@ -179,6 +192,11 @@ public void delete(@PathParam("id") final Long id) { if (Artifact.count("repository.id = ?1", id) > 0) throw new ClientErrorException("Cannot delete repository with artifacts", HttpStatus.SC_CONFLICT); + final Repository repository = Repository.findById(id); + if (repository != null) { + processUpdate(repository, UpdateType.DELETE); + } + Repository.deleteById(id); } diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 8d60731..d6d8dc0 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.narayana.jta.TransactionSemantics; import io.quarkus.security.UnauthorizedException; import jakarta.inject.Inject; import jakarta.persistence.criteria.CriteriaBuilder; @@ -35,6 +37,7 @@ import jakarta.ws.rs.core.UriInfo; import org.apache.http.HttpHeaders; import org.apache.http.entity.ContentType; +import org.hibernate.Hibernate; import org.jboss.resteasy.reactive.server.ServerResponseFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,13 +97,19 @@ public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final Au } protected OCIRepository repositoryOrThrow(final String repositoryId) { - final OCIRepository repository = repositoryManager.findRepository(repositoryId, PackageManager.OCI); - if (repository == null) throw new NotFoundException( - Response.status(Response.Status.NOT_FOUND) - .entity(OCIError.of(OCIErrorCodes.NAME_UNKNOWN, "Unknown repository", "The specified repository does not exist")) - .build() - ); - return repository; + return QuarkusTransaction.runner(TransactionSemantics.JOIN_EXISTING).call(() -> { + final OCIRepository repository = repositoryManager.findRepository(repositoryId, PackageManager.OCI); + if (repository == null) throw new NotFoundException( + Response.status(Response.Status.NOT_FOUND) + .entity(OCIError.of(OCIErrorCodes.NAME_UNKNOWN, "Unknown repository", "The specified repository does not exist")) + .build() + ); + + Hibernate.initialize(repository.getInfo().permissions); + return repository; + }); + + } @GET @@ -141,7 +150,6 @@ protected Response createTokenResponse(final String token, final Duration durati } @GET - @Transactional public Response get(final @PathParam("repositoryId") String repositoryId) { final OCIRepository repository = repositoryOrThrow(repositoryId); final User user = authenticatedUser.getSelf(); @@ -209,7 +217,6 @@ protected StreamHandle getBlob(final OCIRepository repository, } @GET - @Transactional @Path("/{namespace: .*}/blobs/{digest}") public Response getBlob(final @PathParam("repositoryId") String repositoryId, final @PathParam("namespace") String namespace, @@ -229,7 +236,6 @@ public Response getBlob(final @PathParam("repositoryId") String repositoryId, } @GET - @Transactional @Path("/{namespace: .*}/manifests/{reference}") public Response getManifest(final @PathParam("repositoryId") String repositoryId, final @PathParam("namespace") String namespace, @@ -250,7 +256,6 @@ public Response getManifest(final @PathParam("repositoryId") String repositoryId } @POST - @Transactional @Path("/{namespace: .*}/blobs/uploads") public Response createUpload(final @PathParam("repositoryId") String repositoryId, final @PathParam("namespace") String namespace, @@ -293,7 +298,6 @@ public Response createUpload(final @PathParam("repositoryId") String repositoryI } @PATCH - @Transactional @Path("/{namespace: .*}/blobs/uploads/{sessionId}") public Response uploadChunk(final @PathParam("repositoryId") String repositoryId, final @PathParam("namespace") String namespace, @@ -336,7 +340,6 @@ public Response uploadChunk(final @PathParam("repositoryId") String repositoryId } @PUT - @Transactional @Path("/{namespace: .*}/blobs/uploads/{sessionId}") public Response completeUpload(final @PathParam("repositoryId") String repositoryId, final @PathParam("namespace") String namespace, @@ -368,12 +371,11 @@ public Response completeUpload(final @PathParam("repositoryId") String repositor } @GET - @Transactional @Path("/{namespace: .*}/blobs/uploads/{sessionId}") - public Response completeUpload(final @PathParam("repositoryId") String repositoryId, - final @PathParam("namespace") String namespace, - final @PathParam("sessionId") UUID sessionId, - final @QueryParam("uploadId") String uploadId) { + public Response getUploadStatus(final @PathParam("repositoryId") String repositoryId, + final @PathParam("namespace") String namespace, + final @PathParam("sessionId") UUID sessionId, + final @QueryParam("uploadId") String uploadId) { final OCIRepository repository = repositoryOrThrow(repositoryId); final User user = authenticatedUser.getSelf(); diff --git a/server/src/main/webui/src/components/repository/OCIConfigForm.tsx b/server/src/main/webui/src/components/repository/OCIConfigForm.tsx index cb78a8f..8fa3c59 100644 --- a/server/src/main/webui/src/components/repository/OCIConfigForm.tsx +++ b/server/src/main/webui/src/components/repository/OCIConfigForm.tsx @@ -1,11 +1,24 @@ -import { z } from "zod"; -import { type Control, type FieldPath, type FieldValues } from "react-hook-form"; -import { Card, CardContent } from "@/components/ui/card.tsx"; -import { s3Schema, S3ConfigForm } from "@/components/repository/S3ConfigForm.tsx"; -import { FormField } from "@/components/form-field.tsx"; +import {z} from "zod"; +import {type Control, Controller, type FieldPath, type FieldValues, useWatch} from "react-hook-form"; +import {Card, CardContent} from "@/components/ui/card.tsx"; +import {S3ConfigForm, s3Schema} from "@/components/repository/S3ConfigForm.tsx"; +import {FormField} from "@/components/form-field.tsx"; +import {useEffect, useState} from "react"; +import {SystemEndpointApi} from "@/generated"; +import {Switch} from "@/components/ui/switch.tsx"; export const ociSchema = z.object({ s3Config: s3Schema, + routingConfig: z.object({ + enabled: z.boolean(), + targetService: z.string().optional(), + targetPort: z.coerce.number().min(1).max(65535).optional(), + gatewayName: z.string().optional(), + gatewayNamespace: z.string().optional(), + }).refine(data => !data.enabled || (data.targetService && data.targetPort && data.gatewayName && data.gatewayNamespace), { + message: "All routing fields are required when routing is enabled", + path: ["enabled"] + }), }); export type OCIConfig = z.infer; @@ -17,6 +30,13 @@ export const defaultOCIConfig: OCIConfig = { bucket: "", accessKey: "", secretKey: "" + }, + routingConfig: { + enabled: false, + targetService: "", + targetPort: 80, + gatewayName: "", + gatewayNamespace: "" } }; @@ -26,6 +46,24 @@ interface OCIConfigFormProps { } export function OCIConfigForm({ control, prefix }: OCIConfigFormProps) { + const [k8sRoutingSupported, setK8sRoutingSupported] = useState(false); + const routingToggleId = "oci-routing-toggle"; + + useEffect(() => { + new SystemEndpointApi().apiV1SystemK8sCapabilitiesGet() + .then(capabilities => { + setK8sRoutingSupported(capabilities.routing); + }) + .catch(err => { + console.error("Failed to fetch k8s capabilities", err); + }); + }, []); + + const routingEnabled = useWatch({ + control, + name: `${prefix}.routingConfig.enabled` as any + }); + return (
@@ -33,16 +71,74 @@ export function OCIConfigForm({ control, prefi

External Access

- - } - control={control} - placeholder="oci.test.org:8080" - /> -

- The expected host name for connecting to the OCI repository (e.g., host or host:port). -

+ +
+ } + control={control} + placeholder="oci.test.org:8080" + /> +

+ The expected host name for connecting to the OCI repository (e.g., host or host:port). +

+
+ + {k8sRoutingSupported && ( +
+
+
+ +

+ Automatically create a Kubernetes HTTPRoute for this repository. +

+
+
+ ( + + )} + /> +
+
+ + {routingEnabled && ( +
+ + + + +
+ )} +
+ )}
diff --git a/server/src/main/webui/src/view/dashboard/RepositoryEditView.tsx b/server/src/main/webui/src/view/dashboard/RepositoryEditView.tsx index 9aaa997..28d9ae5 100644 --- a/server/src/main/webui/src/view/dashboard/RepositoryEditView.tsx +++ b/server/src/main/webui/src/view/dashboard/RepositoryEditView.tsx @@ -54,6 +54,7 @@ export default function RepositoryEditView() { const pmConfig = CONFIG_MAPPING[selectedPackageManager]; const [initialPermissions, setInitialPermissions] = useState([]); + const [initialMetadata, setInitialMetadata] = useState>({}); useEffect(() => { if (isEdit) { @@ -92,7 +93,12 @@ export default function RepositoryEditView() { setInitialPermissions([...mappedPermissions]); if (repo.metadata) { - form.setValue("externalHost", repo.metadata["HOST_NAME"]); + setInitialMetadata(repo.metadata as any); + if ((repo.metadata as any)["HOST_NAME"]) { + form.setValue("externalHost", (repo.metadata as any)["HOST_NAME"]); + } + } else { + setInitialMetadata({}); } if (repo.settings) { @@ -117,6 +123,12 @@ export default function RepositoryEditView() { ...(settings.mirrorConfig || {}) }; } + if (repo.packageManager === PackageManager.Oci) { + mergedSettings.routingConfig = { + ...(currentPmConfig.defaultValues.routingConfig || {}), + ...(settings.routingConfig || {}) + }; + } } form.setValue(currentPmConfig.configKey as keyof DynamicFormValues, mergedSettings); @@ -143,9 +155,16 @@ export default function RepositoryEditView() { const config = currentPmConfig ? data[currentPmConfig.configKey as keyof DynamicFormValues] : undefined; const settings = config ? JSON.stringify(config) : undefined; - const metadata = data.packageManager === PackageManager.Oci ? { - "HOST_NAME": data.externalHost - } : undefined; + + // Preserve existing metadata and override only the keys managed by this form + const mergedMetadata: Record = isEdit ? { ...initialMetadata } : {}; + if (data.packageManager === PackageManager.Oci) { + if (data.externalHost && data.externalHost.trim().length > 0) { + mergedMetadata["HOST_NAME"] = data.externalHost.trim(); + } + } + // Avoid sending undefined which would wipe metadata server-side + const metadata = mergedMetadata; try { let repoId: number; From 3943ff4d68693ea72a7bbfbb2168ea185991ee8e Mon Sep 17 00:00:00 2001 From: bethibande Date: Wed, 11 Feb 2026 20:03:57 +0100 Subject: [PATCH 02/29] Kubernetes Gateway API support for routing #36 --- .../repository/k8s/KubernetesSupport.java | 154 ++++++++++++++++++ .../RepositoryApplicationContext.java | 10 ++ .../repository/RepositoryUpdatedNotifier.java | 11 ++ .../repository/repository/UpdateType.java | 9 + .../oci/config/OCIRepositoryConfig.java | 14 ++ .../oci/config/OCIRoutingConfig.java | 13 ++ .../repository/web/api/SystemEndpoint.java | 32 ++++ 7 files changed, 243 insertions(+) create mode 100644 server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java create mode 100644 server/src/main/java/com/bethibande/repository/repository/RepositoryApplicationContext.java create mode 100644 server/src/main/java/com/bethibande/repository/repository/RepositoryUpdatedNotifier.java create mode 100644 server/src/main/java/com/bethibande/repository/repository/UpdateType.java create mode 100644 server/src/main/java/com/bethibande/repository/repository/oci/config/OCIRepositoryConfig.java create mode 100644 server/src/main/java/com/bethibande/repository/repository/oci/config/OCIRoutingConfig.java create mode 100644 server/src/main/java/com/bethibande/repository/web/api/SystemEndpoint.java diff --git a/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java b/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java new file mode 100644 index 0000000..b50c981 --- /dev/null +++ b/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java @@ -0,0 +1,154 @@ +package com.bethibande.repository.k8s; + +import com.bethibande.repository.jpa.repository.PackageManager; +import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReview; +import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReviewBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRoute; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.HTTPRouteRuleBuilder; +import io.fabric8.kubernetes.api.model.gatewayapi.v1.ParentReferenceBuilder; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Startup +@ApplicationScoped +public class KubernetesSupport { + + private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesSupport.class); + + public static final String GATEWAY_API_GROUP = "gateway.networking.k8s.io"; + public static final String HTTP_ROUTE_NAME = "httproutes"; + + @Inject + protected KubernetesClient client; + + private String namespace; + + protected boolean kubernetesSupport = true; + protected boolean canManageHttpRoutes = false; + + protected boolean canAccessApi() { + try (final KubernetesClient timeoutClient = new KubernetesClientBuilder() + .withConfig(new ConfigBuilder() + .withConnectionTimeout(200) + .withRequestTimeout(200) + .withRequestRetryBackoffLimit(0) + .build()) + .build()) { + + timeoutClient.getApiGroups(); + return true; + } catch (final Throwable _) { + return false; + } + } + + @PostConstruct + protected void init() { + if (this.client == null) return; + if (!canAccessApi()) { + this.kubernetesSupport = false; + LOGGER.info("Disabled kubernetes support"); + return; + } + + this.namespace = client.getNamespace(); + + this.canManageHttpRoutes = hasPermission("create", HTTP_ROUTE_NAME, GATEWAY_API_GROUP) + && hasPermission("delete", HTTP_ROUTE_NAME, GATEWAY_API_GROUP) + && hasPermission("get", HTTP_ROUTE_NAME, GATEWAY_API_GROUP) + && hasPermission("patch", HTTP_ROUTE_NAME, GATEWAY_API_GROUP) + && hasPermission("list", HTTP_ROUTE_NAME, GATEWAY_API_GROUP); + } + + public boolean isEnabled() { + return this.kubernetesSupport; + } + + public boolean hasHttpRouteSupport() { + return this.canManageHttpRoutes; + } + + protected String toHttpRouteName(final String repository, final PackageManager packageManager) { + return "%s-%s".formatted(packageManager.name().toLowerCase(), repository); + } + + public void deleteRepositoryHttpRouteIfExists(final String repository, final PackageManager packageManager) { + this.client.resources(HTTPRoute.class) + .inNamespace(this.namespace) + .withName(toHttpRouteName(repository, packageManager)) + .delete(); + } + + public void createOrUpdateRepositoryHttpRoute(final String repository, + final PackageManager packageManager, + final String host, + final String targetService, + final int targetPort, + final String gateway, + final String gatewayNamespace) { + final HTTPRoute route = new HTTPRouteBuilder() + .withNewMetadata() + .withName(toHttpRouteName(repository, packageManager)) + .withNamespace(this.namespace) + .endMetadata() + .withNewSpec() + .withHostnames(host) + .withParentRefs(new ParentReferenceBuilder() + .withName(gateway) + .withNamespace(gatewayNamespace) + .withGroup(GATEWAY_API_GROUP) + .withKind("Gateway") + .build()) + .withRules(new HTTPRouteRuleBuilder() + .addNewMatch() + .withNewPath() + .withType("PathPrefix") + .withValue("/v2") + .endPath() + .endMatch() + .addNewFilter() + .withType("URLRewrite") + .withNewUrlRewrite() + .withNewPath() + .withType("ReplacePrefixMatch") + .withReplacePrefixMatch("/repositories/%s/%s/v2".formatted(packageManager.name().toLowerCase(), repository)) + .endPath() + .endUrlRewrite() + .endFilter() + .addNewBackendRef() + .withName(targetService) + .withPort(targetPort) + .endBackendRef() + .build()) + .endSpec() + .build(); + + client.resource(route).serverSideApply(); + } + + protected boolean hasPermission(final String verb, final String resource, final String group) { + final SelfSubjectAccessReview review = new SelfSubjectAccessReviewBuilder() + .withNewSpec() + .withNewResourceAttributes() + .withNamespace(this.namespace) + .withVerb(verb) + .withGroup(group) + .withResource(resource) + .endResourceAttributes() + .endSpec() + .build(); + + final SelfSubjectAccessReview result = client.resource(review).create(); + + return result.getStatus().getAllowed(); + } + +} diff --git a/server/src/main/java/com/bethibande/repository/repository/RepositoryApplicationContext.java b/server/src/main/java/com/bethibande/repository/repository/RepositoryApplicationContext.java new file mode 100644 index 0000000..9152fb6 --- /dev/null +++ b/server/src/main/java/com/bethibande/repository/repository/RepositoryApplicationContext.java @@ -0,0 +1,10 @@ +package com.bethibande.repository.repository; + +import com.bethibande.repository.k8s.KubernetesSupport; +import com.fasterxml.jackson.databind.ObjectMapper; + +public record RepositoryApplicationContext( + ObjectMapper objectMapper, + KubernetesSupport kubernetesSupport +) { +} diff --git a/server/src/main/java/com/bethibande/repository/repository/RepositoryUpdatedNotifier.java b/server/src/main/java/com/bethibande/repository/repository/RepositoryUpdatedNotifier.java new file mode 100644 index 0000000..005efb2 --- /dev/null +++ b/server/src/main/java/com/bethibande/repository/repository/RepositoryUpdatedNotifier.java @@ -0,0 +1,11 @@ +package com.bethibande.repository.repository; + +/** + * An interface that extends {@link ManagedRepository} to provide additional functionality + * for handling repository update notifications. + */ +public interface RepositoryUpdatedNotifier extends ManagedRepository { + + void processUpdate(final UpdateType type); + +} diff --git a/server/src/main/java/com/bethibande/repository/repository/UpdateType.java b/server/src/main/java/com/bethibande/repository/repository/UpdateType.java new file mode 100644 index 0000000..28e7a0f --- /dev/null +++ b/server/src/main/java/com/bethibande/repository/repository/UpdateType.java @@ -0,0 +1,9 @@ +package com.bethibande.repository.repository; + +public enum UpdateType { + + CREATE, + UPDATE, + DELETE + +} diff --git a/server/src/main/java/com/bethibande/repository/repository/oci/config/OCIRepositoryConfig.java b/server/src/main/java/com/bethibande/repository/repository/oci/config/OCIRepositoryConfig.java new file mode 100644 index 0000000..0d1cf80 --- /dev/null +++ b/server/src/main/java/com/bethibande/repository/repository/oci/config/OCIRepositoryConfig.java @@ -0,0 +1,14 @@ +package com.bethibande.repository.repository.oci.config; + +import com.bethibande.repository.repository.S3Config; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.validation.constraints.NotNull; + +@RegisterForReflection +public record OCIRepositoryConfig( + @NotNull + S3Config s3Config, + @NotNull + OCIRoutingConfig routingConfig +) { +} diff --git a/server/src/main/java/com/bethibande/repository/repository/oci/config/OCIRoutingConfig.java b/server/src/main/java/com/bethibande/repository/repository/oci/config/OCIRoutingConfig.java new file mode 100644 index 0000000..6ec2084 --- /dev/null +++ b/server/src/main/java/com/bethibande/repository/repository/oci/config/OCIRoutingConfig.java @@ -0,0 +1,13 @@ +package com.bethibande.repository.repository.oci.config; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection +public record OCIRoutingConfig( + boolean enabled, + String targetService, + int targetPort, + String gatewayName, + String gatewayNamespace +) { +} diff --git a/server/src/main/java/com/bethibande/repository/web/api/SystemEndpoint.java b/server/src/main/java/com/bethibande/repository/web/api/SystemEndpoint.java new file mode 100644 index 0000000..ce9ed97 --- /dev/null +++ b/server/src/main/java/com/bethibande/repository/web/api/SystemEndpoint.java @@ -0,0 +1,32 @@ +package com.bethibande.repository.web.api; + +import com.bethibande.repository.k8s.KubernetesSupport; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@RolesAllowed("ADMIN") +@Path("/api/v1/system") +public class SystemEndpoint { + + @Inject + protected KubernetesSupport kubernetesSupport; + + public record KubernetesCapabilities( + @NotNull boolean enabled, + @NotNull boolean routing + ) { + } + + @GET + @Path("/k8s/capabilities") + public @NotNull KubernetesCapabilities hasKubernetesRoutingSupport() { + return new KubernetesCapabilities( + kubernetesSupport.isEnabled(), + kubernetesSupport.hasHttpRouteSupport() + ); + } + +} From e59f23068b16824f5fd4b23a9d14e65391f5f6bd Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 10:09:36 +0100 Subject: [PATCH 03/29] Kubernetes Gateway API support for routing #36 --- docker-compose.yaml | 2 +- .../repository/web/repositories/OCIRepositoryEndpoint.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 86bf743..9553b3b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,7 +33,7 @@ services: volumes: - minio:/data repository: - image: maxbb/server:1.0-SNAPSHOT + image: bethibandes/repository:latest restart: unless-stopped ports: - "8080:8080" diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index d6d8dc0..838be2b 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -365,7 +365,7 @@ public Response completeUpload(final @PathParam("repositoryId") String repositor return Response.created(URI.create(url)) .build(); } catch (final Throwable th) { - System.err.println(th.getMessage()); + LOGGER.error("Error completing upload", th); return Response.serverError().build(); } } From 48009f6ca15b178909afc7d8fe336bd191a42b95 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 10:21:33 +0100 Subject: [PATCH 04/29] Kubernetes Gateway API support for routing #36 --- .../repositories/OCIRepositoryEndpoint.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 838be2b..f0243c5 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -349,25 +349,20 @@ public Response completeUpload(final @PathParam("repositoryId") String repositor final @QueryParam("part") int partNumber, final @HeaderParam(HttpHeaders.CONTENT_LENGTH) Long contentLength, final InputStream content) { - try { - final OCIRepository repository = repositoryOrThrow(repositoryId); - final User user = authenticatedUser.getSelf(); - final UploadSessionHandle handle = new UploadSessionHandle(sessionId, uploadId); + final OCIRepository repository = repositoryOrThrow(repositoryId); + final User user = authenticatedUser.getSelf(); + final UploadSessionHandle handle = new UploadSessionHandle(sessionId, uploadId); - if (contentLength != null && contentLength > 0) { - final StreamHandle streamHandle = new StreamHandle(content, ContentType.APPLICATION_OCTET_STREAM.getMimeType(), contentLength); - repository.uploadPart(user, namespace, handle, partNumber, streamHandle); - } + if (contentLength != null && contentLength > 0) { + final StreamHandle streamHandle = new StreamHandle(content, ContentType.APPLICATION_OCTET_STREAM.getMimeType(), contentLength); + repository.uploadPart(user, namespace, handle, partNumber, streamHandle); + } - repository.completeUploadSession(user, namespace, digest, handle); + repository.completeUploadSession(user, namespace, digest, handle); - final String url = "/v2/%s/blobs/%s".formatted(namespace, digest); - return Response.created(URI.create(url)) - .build(); - } catch (final Throwable th) { - LOGGER.error("Error completing upload", th); - return Response.serverError().build(); - } + final String url = "/v2/%s/blobs/%s".formatted(namespace, digest); + return Response.created(URI.create(url)) + .build(); } @GET From 02d5d948dd9529651b0b76428558acadcb55f662 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 10:37:54 +0100 Subject: [PATCH 05/29] Kubernetes Gateway API support for routing #36 --- .../repositories/OCIRepositoryEndpoint.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index f0243c5..bcd31ff 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -349,20 +349,25 @@ public Response completeUpload(final @PathParam("repositoryId") String repositor final @QueryParam("part") int partNumber, final @HeaderParam(HttpHeaders.CONTENT_LENGTH) Long contentLength, final InputStream content) { - final OCIRepository repository = repositoryOrThrow(repositoryId); - final User user = authenticatedUser.getSelf(); - final UploadSessionHandle handle = new UploadSessionHandle(sessionId, uploadId); + try { + final OCIRepository repository = repositoryOrThrow(repositoryId); + final User user = authenticatedUser.getSelf(); + final UploadSessionHandle handle = new UploadSessionHandle(sessionId, uploadId); - if (contentLength != null && contentLength > 0) { - final StreamHandle streamHandle = new StreamHandle(content, ContentType.APPLICATION_OCTET_STREAM.getMimeType(), contentLength); - repository.uploadPart(user, namespace, handle, partNumber, streamHandle); - } + if (contentLength != null && contentLength > 0) { + final StreamHandle streamHandle = new StreamHandle(content, ContentType.APPLICATION_OCTET_STREAM.getMimeType(), contentLength); + repository.uploadPart(user, namespace, handle, partNumber, streamHandle); + } - repository.completeUploadSession(user, namespace, digest, handle); + repository.completeUploadSession(user, namespace, digest, handle); - final String url = "/v2/%s/blobs/%s".formatted(namespace, digest); - return Response.created(URI.create(url)) - .build(); + final String url = "/v2/%s/blobs/%s".formatted(namespace, digest); + return Response.created(URI.create(url)) + .build(); + } catch (final Throwable th) { + LOGGER.error("Error completing upload", th); + throw th; + } } @GET From 7fe549ab182d715ed461db5f28dd193caffc4e9a Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 11:01:35 +0100 Subject: [PATCH 06/29] Kubernetes Gateway API support for routing #36 --- .../repository/web/repositories/OCIRepositoryEndpoint.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index bcd31ff..ba6b77f 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -150,6 +150,7 @@ protected Response createTokenResponse(final String token, final Duration durati } @GET + @Transactional public Response get(final @PathParam("repositoryId") String repositoryId) { final OCIRepository repository = repositoryOrThrow(repositoryId); final User user = authenticatedUser.getSelf(); From f46d80b7248ffadadb5ec8dc9b1b2913cdb447f0 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 13:04:35 +0100 Subject: [PATCH 07/29] Kubernetes Gateway API support for routing #36 debug --- .../repository/web/repositories/OCIRepositoryEndpoint.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index ba6b77f..bbd0000 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -73,6 +73,9 @@ public class OCIRepositoryEndpoint { @ServerResponseFilter public void authResponseInterceptor(final ContainerResponseContext context) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; + + LOGGER.debug("Intercepting response for {} with status {}", uriInfo.getPath(), context.getStatus()); + if (context.getStatus() == 401) { String baseUri = uriInfo.getBaseUri().toString(); if (baseUri.endsWith("/")) { From 15cd8cfa1fed1f2bef3377a6ff5b531dc663f41d Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 14:33:37 +0100 Subject: [PATCH 08/29] Kubernetes Gateway API support for routing #36 debug --- .../repository/web/repositories/OCIRepositoryEndpoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index bbd0000..95354e7 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -74,7 +74,7 @@ public class OCIRepositoryEndpoint { public void authResponseInterceptor(final ContainerResponseContext context) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; - LOGGER.debug("Intercepting response for {} with status {}", uriInfo.getPath(), context.getStatus()); + LOGGER.info("Intercepting response for {} with status {}", uriInfo.getPath(), context.getStatus()); if (context.getStatus() == 401) { String baseUri = uriInfo.getBaseUri().toString(); From b6cfe413d8ad4d3fc9bfd93377799b5ff4b24e3d Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 14:49:22 +0100 Subject: [PATCH 09/29] Kubernetes Gateway API support for routing #36 --- .../repository/web/repositories/OCIRepositoryEndpoint.java | 2 -- .../bethibande/repository/web/repositories/oci/OCIError.java | 3 +++ .../repository/web/repositories/oci/OCIErrorEntry.java | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 95354e7..05ae00c 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -74,8 +74,6 @@ public class OCIRepositoryEndpoint { public void authResponseInterceptor(final ContainerResponseContext context) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; - LOGGER.info("Intercepting response for {} with status {}", uriInfo.getPath(), context.getStatus()); - if (context.getStatus() == 401) { String baseUri = uriInfo.getBaseUri().toString(); if (baseUri.endsWith("/")) { diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/oci/OCIError.java b/server/src/main/java/com/bethibande/repository/web/repositories/oci/OCIError.java index 9764365..2a4e60e 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/oci/OCIError.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/oci/OCIError.java @@ -1,7 +1,10 @@ package com.bethibande.repository.web.repositories.oci; +import io.quarkus.runtime.annotations.RegisterForReflection; + import java.util.List; +@RegisterForReflection public record OCIError( List errors ) { diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/oci/OCIErrorEntry.java b/server/src/main/java/com/bethibande/repository/web/repositories/oci/OCIErrorEntry.java index cebdb58..0354ad3 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/oci/OCIErrorEntry.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/oci/OCIErrorEntry.java @@ -1,5 +1,8 @@ package com.bethibande.repository.web.repositories.oci; +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection public record OCIErrorEntry( String code, String message, From 9f57f0de1d58722050bc63235f6ab9831b8cce79 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 14:50:31 +0100 Subject: [PATCH 10/29] Kubernetes Gateway API support for routing #36 --- .../repositories/OCIRepositoryEndpoint.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 05ae00c..b6cb3eb 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -351,25 +351,20 @@ public Response completeUpload(final @PathParam("repositoryId") String repositor final @QueryParam("part") int partNumber, final @HeaderParam(HttpHeaders.CONTENT_LENGTH) Long contentLength, final InputStream content) { - try { - final OCIRepository repository = repositoryOrThrow(repositoryId); - final User user = authenticatedUser.getSelf(); - final UploadSessionHandle handle = new UploadSessionHandle(sessionId, uploadId); + final OCIRepository repository = repositoryOrThrow(repositoryId); + final User user = authenticatedUser.getSelf(); + final UploadSessionHandle handle = new UploadSessionHandle(sessionId, uploadId); - if (contentLength != null && contentLength > 0) { - final StreamHandle streamHandle = new StreamHandle(content, ContentType.APPLICATION_OCTET_STREAM.getMimeType(), contentLength); - repository.uploadPart(user, namespace, handle, partNumber, streamHandle); - } + if (contentLength != null && contentLength > 0) { + final StreamHandle streamHandle = new StreamHandle(content, ContentType.APPLICATION_OCTET_STREAM.getMimeType(), contentLength); + repository.uploadPart(user, namespace, handle, partNumber, streamHandle); + } - repository.completeUploadSession(user, namespace, digest, handle); + repository.completeUploadSession(user, namespace, digest, handle); - final String url = "/v2/%s/blobs/%s".formatted(namespace, digest); - return Response.created(URI.create(url)) - .build(); - } catch (final Throwable th) { - LOGGER.error("Error completing upload", th); - throw th; - } + final String url = "/v2/%s/blobs/%s".formatted(namespace, digest); + return Response.created(URI.create(url)) + .build(); } @GET From 429d218b836736a84dafb8b82a5f0445ddc39d99 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 15:02:24 +0100 Subject: [PATCH 11/29] Kubernetes Gateway API support for routing #36 --- .../repository/security/BearerTokenAuthenticationRequest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/main/java/com/bethibande/repository/security/BearerTokenAuthenticationRequest.java b/server/src/main/java/com/bethibande/repository/security/BearerTokenAuthenticationRequest.java index 9da2daf..4ef51bc 100644 --- a/server/src/main/java/com/bethibande/repository/security/BearerTokenAuthenticationRequest.java +++ b/server/src/main/java/com/bethibande/repository/security/BearerTokenAuthenticationRequest.java @@ -1,7 +1,9 @@ package com.bethibande.repository.security; +import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.security.identity.request.BaseAuthenticationRequest; +@RegisterForReflection public class BearerTokenAuthenticationRequest extends BaseAuthenticationRequest { private final String accessToken; From 242701639f3cd8cd6dfacdd99e170713b89bbad8 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 16:25:50 +0100 Subject: [PATCH 12/29] Kubernetes Gateway API support for routing #36 more debugging... --- .../repository/security/UserAuthenticationMechanism.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java b/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java index 1c703f5..cfdb4df 100644 --- a/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java +++ b/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java @@ -15,6 +15,8 @@ import io.vertx.ext.web.RoutingContext; import jakarta.enterprise.context.ApplicationScoped; import org.apache.http.HttpHeaders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Base64; import java.util.Optional; @@ -22,6 +24,8 @@ @ApplicationScoped public class UserAuthenticationMechanism implements HttpAuthenticationMechanism { + private static final Logger LOGGER = LoggerFactory.getLogger(UserAuthenticationMechanism.class); + public static final String COOKIE_NAME = "Identity"; protected AuthenticationRequest cookieAuth(final Cookie cookie) { @@ -45,11 +49,13 @@ public Uni authenticate(final RoutingContext context, final Id final Cookie cookie = context.request().getCookie(COOKIE_NAME); if (cookie != null) { + LOGGER.info("Identity {}", cookie.getValue()); request = cookieAuth(cookie); } final String authorization = context.request().getHeader(HttpHeaders.AUTHORIZATION); if (authorization != null) { + LOGGER.info("Authorization {}", authorization); if (authorization.startsWith("Basic ")) { request = basicAuth(authorization.substring(6)); } From b6f369a432603dfb2a7ceedd89082f2557e0b836 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 16:58:26 +0100 Subject: [PATCH 13/29] Kubernetes Gateway API support for routing #36 --- docker-compose.yaml | 2 +- .../src/main/java/com/bethibande/repository/jpa/user/User.java | 2 ++ .../repository/security/UserAuthenticationMechanism.java | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 9553b3b..8c4c35e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,7 +33,7 @@ services: volumes: - minio:/data repository: - image: bethibandes/repository:latest + image: bethibande/repository:latest restart: unless-stopped ports: - "8080:8080" diff --git a/server/src/main/java/com/bethibande/repository/jpa/user/User.java b/server/src/main/java/com/bethibande/repository/jpa/user/User.java index d0a4c0c..6654228 100644 --- a/server/src/main/java/com/bethibande/repository/jpa/user/User.java +++ b/server/src/main/java/com/bethibande/repository/jpa/user/User.java @@ -2,6 +2,7 @@ import com.bethibande.process.annotation.EntityDTO; import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.persistence.*; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; @@ -14,6 +15,7 @@ @Entity @Indexed +@RegisterForReflection @Table(name = "Users") @EntityDTO(excludeProperties = "id") @EntityDTO(excludeProperties = "password") diff --git a/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java b/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java index cfdb4df..70b05fc 100644 --- a/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java +++ b/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java @@ -49,13 +49,11 @@ public Uni authenticate(final RoutingContext context, final Id final Cookie cookie = context.request().getCookie(COOKIE_NAME); if (cookie != null) { - LOGGER.info("Identity {}", cookie.getValue()); request = cookieAuth(cookie); } final String authorization = context.request().getHeader(HttpHeaders.AUTHORIZATION); if (authorization != null) { - LOGGER.info("Authorization {}", authorization); if (authorization.startsWith("Basic ")) { request = basicAuth(authorization.substring(6)); } From da599499d650a93e78d213d8a8519084b30ce2a7 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 17:25:41 +0100 Subject: [PATCH 14/29] Kubernetes Gateway API support for routing #36 fix pipeline --- .github/workflows/main.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 05c42c1..6c46233 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -87,13 +87,11 @@ jobs: - name: Create and Push Multi-Arch Manifest run: | - docker manifest create bethibande/repository:${{ github.event.inputs.version }} \ - --amend bethibande/repository:${{ github.event.inputs.version }}-amd64 \ - --amend bethibande/repository:${{ github.event.inputs.version }}-arm64 - - docker manifest create bethibande/repository:latest \ - --amend bethibande/repository:${{ github.event.inputs.version }}-amd64 \ - --amend bethibande/repository:${{ github.event.inputs.version }}-arm64 - - docker manifest push bethibande/repository:${{ github.event.inputs.version }} - docker manifest push bethibande/repository:latest + docker buildx imagetools create -t bethibande/repository:${{ github.event.inputs.version }} \ + bethibande/repository:${{ github.event.inputs.version }}-amd64 \ + bethibande/repository:${{ github.event.inputs.version }}-arm64 + + # Create the latest tag + docker buildx imagetools create -t bethibande/repository:latest \ + bethibande/repository:${{ github.event.inputs.version }}-amd64 \ + bethibande/repository:${{ github.event.inputs.version }}-arm64 From 7236ec784606dc7e08e44287b5e72bb04c8aa322 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 17:43:36 +0100 Subject: [PATCH 15/29] Kubernetes Gateway API support for routing #36 --- .../security/UserAuthenticationMechanism.java | 4 ---- .../web/repositories/OCIRepositoryEndpoint.java | 16 ++++++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java b/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java index 70b05fc..1c703f5 100644 --- a/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java +++ b/server/src/main/java/com/bethibande/repository/security/UserAuthenticationMechanism.java @@ -15,8 +15,6 @@ import io.vertx.ext.web.RoutingContext; import jakarta.enterprise.context.ApplicationScoped; import org.apache.http.HttpHeaders; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.Base64; import java.util.Optional; @@ -24,8 +22,6 @@ @ApplicationScoped public class UserAuthenticationMechanism implements HttpAuthenticationMechanism { - private static final Logger LOGGER = LoggerFactory.getLogger(UserAuthenticationMechanism.class); - public static final String COOKIE_NAME = "Identity"; protected AuthenticationRequest cookieAuth(final Cookie cookie) { diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index b6cb3eb..1eded48 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -70,10 +70,20 @@ public class OCIRepositoryEndpoint { @Context protected UriInfo uriInfo; + @Inject + public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final AuthenticatedUser authenticatedUser) { + this.repositoryManager = repositoryManager; + this.authenticatedUser = authenticatedUser; + } + @ServerResponseFilter public void authResponseInterceptor(final ContainerResponseContext context) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; + final String auth = context.getHeaderString(HttpHeaders.AUTHORIZATION); + final User user = authenticatedUser.getSelf(); + LOGGER.info("Path: {}; User: {}; Auth: {}", uriInfo.getPath(), user != null ? user.name : "Anonymous", auth); + if (context.getStatus() == 401) { String baseUri = uriInfo.getBaseUri().toString(); if (baseUri.endsWith("/")) { @@ -91,12 +101,6 @@ public void authResponseInterceptor(final ContainerResponseContext context) { } } - @Inject - public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final AuthenticatedUser authenticatedUser) { - this.repositoryManager = repositoryManager; - this.authenticatedUser = authenticatedUser; - } - protected OCIRepository repositoryOrThrow(final String repositoryId) { return QuarkusTransaction.runner(TransactionSemantics.JOIN_EXISTING).call(() -> { final OCIRepository repository = repositoryManager.findRepository(repositoryId, PackageManager.OCI); From 3aeb665ca60ad936900c6e5992476ae62be3123b Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 18:00:49 +0100 Subject: [PATCH 16/29] Kubernetes Gateway API support for routing #36 more debugging.. --- .../repository/web/repositories/OCIRepositoryEndpoint.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 1eded48..ebc4f26 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -24,6 +24,7 @@ import io.quarkus.narayana.jta.QuarkusTransaction; import io.quarkus.narayana.jta.TransactionSemantics; import io.quarkus.security.UnauthorizedException; +import io.vertx.ext.web.RoutingContext; import jakarta.inject.Inject; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; @@ -77,12 +78,12 @@ public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final Au } @ServerResponseFilter - public void authResponseInterceptor(final ContainerResponseContext context) { + public void authResponseInterceptor(final ContainerResponseContext context, final RoutingContext routing) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; - final String auth = context.getHeaderString(HttpHeaders.AUTHORIZATION); + final String auth = routing.request().getHeader(HttpHeaders.AUTHORIZATION); final User user = authenticatedUser.getSelf(); - LOGGER.info("Path: {}; User: {}; Auth: {}", uriInfo.getPath(), user != null ? user.name : "Anonymous", auth); + LOGGER.info("Method: {}; Path: {}; User: {}; Auth: {}", routing.request().method(), uriInfo.getPath(), user != null ? user.name : "Anonymous", auth); if (context.getStatus() == 401) { String baseUri = uriInfo.getBaseUri().toString(); From c4f9c57152a78073732a25f883df097bcdfa1383 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 18:21:18 +0100 Subject: [PATCH 17/29] Kubernetes Gateway API support for routing #36 more debugging... --- .../repository/web/repositories/OCIRepositoryEndpoint.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index ebc4f26..832a787 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -81,6 +81,9 @@ public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final Au public void authResponseInterceptor(final ContainerResponseContext context, final RoutingContext routing) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; + String proto = context.getHeaderString("X-Forwarded-Proto"); + LOGGER.info("External Protocol: {}", proto); + final String auth = routing.request().getHeader(HttpHeaders.AUTHORIZATION); final User user = authenticatedUser.getSelf(); LOGGER.info("Method: {}; Path: {}; User: {}; Auth: {}", routing.request().method(), uriInfo.getPath(), user != null ? user.name : "Anonymous", auth); From 8e2a473d87012bc4ab7c4a6929edce0d8ee1bf34 Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 18:37:16 +0100 Subject: [PATCH 18/29] Kubernetes Gateway API support for routing #36 --- .../com/bethibande/repository/k8s/KubernetesSupport.java | 9 +++++++++ .../web/repositories/OCIRepositoryEndpoint.java | 7 ------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java b/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java index b50c981..aa48be2 100644 --- a/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java +++ b/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java @@ -115,6 +115,15 @@ public void createOrUpdateRepositoryHttpRoute(final String repository, .endPath() .endMatch() .addNewFilter() + .withType("RequestHeaderModifier") + .withNewRequestHeaderModifier() + .addNewSet() + .withName("X-Forwarded-Proto") + .withValue("https") + .endSet() + .endRequestHeaderModifier() + .endFilter() + .addNewFilter() .withType("URLRewrite") .withNewUrlRewrite() .withNewPath() diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 832a787..9337f24 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -81,13 +81,6 @@ public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final Au public void authResponseInterceptor(final ContainerResponseContext context, final RoutingContext routing) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; - String proto = context.getHeaderString("X-Forwarded-Proto"); - LOGGER.info("External Protocol: {}", proto); - - final String auth = routing.request().getHeader(HttpHeaders.AUTHORIZATION); - final User user = authenticatedUser.getSelf(); - LOGGER.info("Method: {}; Path: {}; User: {}; Auth: {}", routing.request().method(), uriInfo.getPath(), user != null ? user.name : "Anonymous", auth); - if (context.getStatus() == 401) { String baseUri = uriInfo.getBaseUri().toString(); if (baseUri.endsWith("/")) { From 5f049925258f5ec41c51707728fcb6a6bee193bd Mon Sep 17 00:00:00 2001 From: bethibande Date: Thu, 12 Feb 2026 18:51:44 +0100 Subject: [PATCH 19/29] Kubernetes Gateway API support for routing #36 and debug info once again --- .../repository/web/repositories/OCIRepositoryEndpoint.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 9337f24..832a787 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -81,6 +81,13 @@ public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final Au public void authResponseInterceptor(final ContainerResponseContext context, final RoutingContext routing) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; + String proto = context.getHeaderString("X-Forwarded-Proto"); + LOGGER.info("External Protocol: {}", proto); + + final String auth = routing.request().getHeader(HttpHeaders.AUTHORIZATION); + final User user = authenticatedUser.getSelf(); + LOGGER.info("Method: {}; Path: {}; User: {}; Auth: {}", routing.request().method(), uriInfo.getPath(), user != null ? user.name : "Anonymous", auth); + if (context.getStatus() == 401) { String baseUri = uriInfo.getBaseUri().toString(); if (baseUri.endsWith("/")) { From 991f8389b7640f72a47399c05da1026f5bda26c6 Mon Sep 17 00:00:00 2001 From: bethibande Date: Fri, 13 Feb 2026 09:17:23 +0100 Subject: [PATCH 20/29] Kubernetes Gateway API support for routing #36 and debug info once again --- .../repository/web/repositories/OCIRepositoryEndpoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 832a787..9fcc7c2 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -81,7 +81,7 @@ public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final Au public void authResponseInterceptor(final ContainerResponseContext context, final RoutingContext routing) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; - String proto = context.getHeaderString("X-Forwarded-Proto"); + final String proto = routing.request().getHeader("X-Forwarded-Proto"); LOGGER.info("External Protocol: {}", proto); final String auth = routing.request().getHeader(HttpHeaders.AUTHORIZATION); From 7d0e59c09c40d5e2ed74c02e6f1bd8decf537dfe Mon Sep 17 00:00:00 2001 From: bethibande Date: Fri, 13 Feb 2026 09:33:57 +0100 Subject: [PATCH 21/29] Kubernetes Gateway API support for routing #36 --- .../web/repositories/OCIRepositoryEndpoint.java | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 9fcc7c2..ec529dd 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -81,18 +81,10 @@ public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final Au public void authResponseInterceptor(final ContainerResponseContext context, final RoutingContext routing) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; - final String proto = routing.request().getHeader("X-Forwarded-Proto"); - LOGGER.info("External Protocol: {}", proto); - - final String auth = routing.request().getHeader(HttpHeaders.AUTHORIZATION); - final User user = authenticatedUser.getSelf(); - LOGGER.info("Method: {}; Path: {}; User: {}; Auth: {}", routing.request().method(), uriInfo.getPath(), user != null ? user.name : "Anonymous", auth); - if (context.getStatus() == 401) { - String baseUri = uriInfo.getBaseUri().toString(); - if (baseUri.endsWith("/")) { - baseUri = baseUri.substring(0, baseUri.length() - 1); - } + final String proto = routing.request().getHeader("X-Forwarded-Proto"); + final boolean https = proto == null || proto.equalsIgnoreCase("https"); + final String baseUri = "%s://%s".formatted(https ? "https" : "http", uriInfo.getBaseUri().getHost()); final String realm = "%s/v2/auth".formatted(baseUri); final String service = uriInfo.getBaseUri().getHost(); From 324a5511a2cc1233729860565a5901778a1ccf90 Mon Sep 17 00:00:00 2001 From: bethibande Date: Fri, 13 Feb 2026 09:42:18 +0100 Subject: [PATCH 22/29] Kubernetes Gateway API support for routing #36 --- .../web/repositories/OCIRepositoryEndpoint.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index ec529dd..48b1533 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -81,10 +81,17 @@ public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final Au public void authResponseInterceptor(final ContainerResponseContext context, final RoutingContext routing) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; + if (context.getStatus() == 401) { + // Quarkus should be handling the X-Forwarded-Proto header, but for some reason it doesn't + // Maybe we are missing some configuration property, + // but for now we need to do this since all redirects turn out as http otherwise final String proto = routing.request().getHeader("X-Forwarded-Proto"); final boolean https = proto == null || proto.equalsIgnoreCase("https"); - final String baseUri = "%s://%s".formatted(https ? "https" : "http", uriInfo.getBaseUri().getHost()); + String baseUri = uriInfo.getBaseUri().toString().replaceAll("http(s)?://", https ? "https://" : "http://"); + if (baseUri.endsWith("/")) { + baseUri = baseUri.substring(0, baseUri.length() - 1); + } final String realm = "%s/v2/auth".formatted(baseUri); final String service = uriInfo.getBaseUri().getHost(); @@ -109,8 +116,6 @@ protected OCIRepository repositoryOrThrow(final String repositoryId) { Hibernate.initialize(repository.getInfo().permissions); return repository; }); - - } @GET From 3d264f2e16dfeff5c53fab7f79c3ad822cd6f02a Mon Sep 17 00:00:00 2001 From: bethibande Date: Fri, 13 Feb 2026 09:51:50 +0100 Subject: [PATCH 23/29] Kubernetes Gateway API support for routing #36... --- .../bethibande/repository/repository/oci/OCIRepository.java | 2 +- .../repository/web/repositories/OCIRepositoryEndpoint.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepository.java b/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepository.java index 991ce19..26e8e54 100644 --- a/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepository.java +++ b/server/src/main/java/com/bethibande/repository/repository/oci/OCIRepository.java @@ -241,7 +241,7 @@ public OCIStreamHandle getManifest(final User user, final String namespace, fina public OCIContentInfo getManifestInfo(final User user, final String namespace, final String reference) { checkViewAccess(user); - if (reference.matches("^sha256:[0-9a-fA-F]{64}$|^sha512:[0-9a-fA-F]{128}$")) { + if (isDigest(reference)) { final ObjectInfo info = this.backend.headObject(toManifestKey(namespace, reference)); if (info == null) return null; return new OCIContentInfo( diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 48b1533..33cbc8e 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -81,6 +81,9 @@ public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final Au public void authResponseInterceptor(final ContainerResponseContext context, final RoutingContext routing) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; + final String auth = routing.request().getHeader(HttpHeaders.AUTHORIZATION); + final User user = authenticatedUser.getSelf(); + LOGGER.info("Method: {}; Path: {}; User: {}; Auth: {}", routing.request().method(), uriInfo.getPath(), user != null ? user.name : "Anonymous", auth); if (context.getStatus() == 401) { // Quarkus should be handling the X-Forwarded-Proto header, but for some reason it doesn't @@ -170,7 +173,6 @@ public Response get(final @PathParam("repositoryId") String repositoryId) { } @HEAD - @Transactional @Path("/{namespace: .*}/blobs/{digest}") public Response headBlob(final @PathParam("repositoryId") String repositoryId, final @PathParam("namespace") String namespace, @@ -189,7 +191,6 @@ public Response headBlob(final @PathParam("repositoryId") String repositoryId, } @HEAD - @Transactional @Path("/{namespace: .*}/manifests/{reference}") public Response headManifest(final @PathParam("repositoryId") String repositoryId, final @PathParam("namespace") String namespace, From 809ea8ac0e030359d0dcd48a53d09c5f53c9de1f Mon Sep 17 00:00:00 2001 From: bethibande Date: Fri, 13 Feb 2026 10:12:03 +0100 Subject: [PATCH 24/29] Kubernetes Gateway API support for routing #36 --- server/src/main/resources/application.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 0814e44..0b21b9e 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -2,6 +2,7 @@ quarkus.liquibase.change-log=classpath:liquibase/master.xml quarkus.liquibase.migrate-at-start=true quarkus.http.limits.max-body-size=10G +quarkus.http.proxy.proxy-address-forwarding=true quarkus.http.proxy.allow-xForwarded=true %dev.search.hosts=localhost:9200 From 03240297c4cd12fa1927f09fde12012fa0fce2b9 Mon Sep 17 00:00:00 2001 From: bethibande Date: Fri, 13 Feb 2026 10:23:12 +0100 Subject: [PATCH 25/29] Kubernetes Gateway API support for routing #36 --- .../web/repositories/OCIRepositoryEndpoint.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java index 33cbc8e..d074266 100644 --- a/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java +++ b/server/src/main/java/com/bethibande/repository/web/repositories/OCIRepositoryEndpoint.java @@ -81,17 +81,8 @@ public OCIRepositoryEndpoint(final RepositoryManager repositoryManager, final Au public void authResponseInterceptor(final ContainerResponseContext context, final RoutingContext routing) { if (!uriInfo.getPath().startsWith("/repositories/oci")) return; - final String auth = routing.request().getHeader(HttpHeaders.AUTHORIZATION); - final User user = authenticatedUser.getSelf(); - LOGGER.info("Method: {}; Path: {}; User: {}; Auth: {}", routing.request().method(), uriInfo.getPath(), user != null ? user.name : "Anonymous", auth); - if (context.getStatus() == 401) { - // Quarkus should be handling the X-Forwarded-Proto header, but for some reason it doesn't - // Maybe we are missing some configuration property, - // but for now we need to do this since all redirects turn out as http otherwise - final String proto = routing.request().getHeader("X-Forwarded-Proto"); - final boolean https = proto == null || proto.equalsIgnoreCase("https"); - String baseUri = uriInfo.getBaseUri().toString().replaceAll("http(s)?://", https ? "https://" : "http://"); + String baseUri = uriInfo.getBaseUri().toString(); if (baseUri.endsWith("/")) { baseUri = baseUri.substring(0, baseUri.length() - 1); } From 9821c47be7d11c27a48bbce90726438ee476f97f Mon Sep 17 00:00:00 2001 From: bethibande Date: Fri, 13 Feb 2026 10:26:44 +0100 Subject: [PATCH 26/29] update build job --- .github/workflows/main.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6c46233..c8efb56 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,15 +56,16 @@ jobs: # The next step will generate these files. # This order is necessary since this step will generate the openapi-spec. - name: Quarkus App Parts Build - run: ./gradlew :server:quarkusAppPartsBuild + run: ./gradlew :server:quarkusAppPartsBuild "-Pversion=${{ github.event.inputs.version }}" continue-on-error: true - name: Generate OpenAPI - run: ./gradlew :server:openApiGenerate + run: ./gradlew :server:openApiGenerate "-Pversion=${{ github.event.inputs.version }}" - name: Build and Push Arch-Specific Image run: | ./gradlew :server:build \ + "-Pversion=${{ github.event.inputs.version }}" "-Dquarkus.native.container-build=true" \ "-Dquarkus.container-image.builder=docker" \ "-Dquarkus.native.enabled=true" \ From fdf1289cef8c470ce03804e63b5d41b433eb8f8b Mon Sep 17 00:00:00 2001 From: bethibande Date: Fri, 13 Feb 2026 10:27:37 +0100 Subject: [PATCH 27/29] updated setup-gradle actions --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c8efb56..970d565 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: distribution: 'temurin' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v5 with: cache-read-only: false From ea98f5ce9681b45381f62cd85204e326396f8613 Mon Sep 17 00:00:00 2001 From: bethibande Date: Fri, 13 Feb 2026 10:31:35 +0100 Subject: [PATCH 28/29] Kubernetes Gateway API support for routing #36 --- .../com/bethibande/repository/k8s/KubernetesSupport.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java b/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java index aa48be2..b50c981 100644 --- a/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java +++ b/server/src/main/java/com/bethibande/repository/k8s/KubernetesSupport.java @@ -115,15 +115,6 @@ public void createOrUpdateRepositoryHttpRoute(final String repository, .endPath() .endMatch() .addNewFilter() - .withType("RequestHeaderModifier") - .withNewRequestHeaderModifier() - .addNewSet() - .withName("X-Forwarded-Proto") - .withValue("https") - .endSet() - .endRequestHeaderModifier() - .endFilter() - .addNewFilter() .withType("URLRewrite") .withNewUrlRewrite() .withNewPath() From bac4a3701af6b37494bdfa84e68334a12d85c9b9 Mon Sep 17 00:00:00 2001 From: bethibande Date: Fri, 13 Feb 2026 10:32:11 +0100 Subject: [PATCH 29/29] fix job --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 970d565..ecbd34b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -65,7 +65,7 @@ jobs: - name: Build and Push Arch-Specific Image run: | ./gradlew :server:build \ - "-Pversion=${{ github.event.inputs.version }}" + "-Pversion=${{ github.event.inputs.version }}" \ "-Dquarkus.native.container-build=true" \ "-Dquarkus.container-image.builder=docker" \ "-Dquarkus.native.enabled=true" \